@ddt-tools/cli 0.2.4 → 0.3.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.
- package/README.md +2 -0
- package/dist/cli.js +49 -11
- package/dist/cli.js.map +1 -1
- package/dist/{compare-P7JOV76O.js → compare-IOEATL6G.js} +69 -13
- package/dist/compare-IOEATL6G.js.map +1 -0
- package/dist/{errorReporting-3LPE2IJY.js → errorReporting-LX6WT4JH.js} +2 -2
- package/dist/{errorReporting-3LPE2IJY.js.map → errorReporting-LX6WT4JH.js.map} +1 -1
- package/dist/import-EGOVKTLX.js +29 -0
- package/dist/import-EGOVKTLX.js.map +1 -0
- package/dist/{import-2RNYDL4E.js → import-script-R5RXPDH6.js} +5 -5
- package/dist/import-script-R5RXPDH6.js.map +1 -0
- package/dist/{mcp-F7FND5X7.js → mcp-6ZXOAF7S.js} +2 -2
- package/dist/{mcp-F7FND5X7.js.map → mcp-6ZXOAF7S.js.map} +1 -1
- package/dist/{publish-AYCRMCE2.js → publish-HLP3XHM5.js} +35 -8
- package/dist/publish-HLP3XHM5.js.map +1 -0
- package/package.json +13 -13
- package/dist/compare-P7JOV76O.js.map +0 -1
- package/dist/import-2RNYDL4E.js.map +0 -1
- package/dist/publish-AYCRMCE2.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/compare.ts","../src/util/color.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n LiveSource,\n PacSource,\n ProjectSource,\n ai,\n compileSlice,\n createConnection,\n getProfile,\n loadProject,\n pac as pacNs,\n renderHtmlReport,\n safety,\n typecheck,\n writeCompareHistory,\n type CompareSource,\n} from '@ddt-tools/core';\nimport { addMappingFlags, buildMappingFromOptions, describeMapping } from '../util/mapping.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\nimport { makeColorizer, resolveColorMode } from '../util/color.js';\n\n/**\n * `ddt compare` — diff two schemas in any direction. Each side may be a\n * `.ddtproj` (parsed in place), a `.ddtpac` (built artifact), or a live\n * workspace via `databricks://<profile>[/catalog[/schema]]` (CLIPARITY.6 —\n * mirrors SDT's project/pac/live compare; `databricks://` is the DDT analogue\n * of `snowflake://`).\n *\n * Supports logical-name mapping (`--map`, `--map-file`) so two schemas whose\n * catalog / schema names differ can compare semantically. See\n * `docs/LOGICAL_NAME_MAPPING.md`.\n */\n\n/**\n * Resolve a polymorphic source/target argument into a `CompareSource`.\n * Supported forms (mirrors SDT's `resolveSource`):\n * - path/to/foo.ddtproj → ProjectSource (parses `.sql` in place)\n * - path/to/foo.ddtpac → PacSource (built artifact)\n * - databricks://<profile>[/catalog[/schema]] → LiveSource (workspace extract)\n */\nasync function resolveSource(arg: string): Promise<CompareSource> {\n if (arg.startsWith('databricks://')) {\n const rest = arg.slice('databricks://'.length);\n const [profileName, catalog, schema] = rest.split('/');\n if (!profileName) throw new Error(`Invalid databricks URI: ${arg}`);\n const profile = await getProfile(profileName);\n const conn = createConnection(profile);\n return new LiveSource(conn, {\n ...(catalog ? { catalog } : {}),\n ...(schema ? { schema } : {}),\n });\n }\n const lower = arg.toLowerCase();\n if (lower.endsWith('.ddtproj')) return new ProjectSource(arg);\n if (lower.endsWith('.ddtpac')) return new PacSource(arg);\n throw new Error(\n `Unrecognized source: ${arg}. Use *.ddtproj, *.ddtpac, or databricks://<profile>[/catalog[/schema]].`,\n );\n}\n\n/**\n * Auto-load a Project Slice from the source argument. A `.ddtproj` carries its\n * slice in the project file; a `.ddtpac` carries it in the manifest. Live\n * (`databricks://`) sources have no slice. Returns `undefined` when there's no\n * slice to apply.\n */\nasync function maybeLoadSliceFromSource(\n arg: string,\n): Promise<ReturnType<typeof compileSlice> | undefined> {\n const lower = arg.toLowerCase();\n if (lower.endsWith('.ddtproj')) {\n const loaded = await loadProject(arg);\n if (!loaded.project.slice) return undefined;\n return compileSlice(loaded.project.slice);\n }\n if (lower.endsWith('.ddtpac')) {\n const srcPac = await pacNs.readPac(arg);\n if (srcPac.manifest.slice) return compileSlice(srcPac.manifest.slice);\n }\n return undefined;\n}\n\n/**\n * AUDITCMP.1 — workspace root for the compare-history record. Project / pac\n * sources anchor the audit trail next to the artifact being compared; live\n * (`databricks://`) sources fall back to CWD. Mirrors SDT's `compareHistoryRoot`.\n */\nfunction compareHistoryRoot(sourceArg: string): string {\n const lower = sourceArg.toLowerCase();\n if (lower.endsWith('.ddtproj') || lower.endsWith('.ddtpac')) {\n return path.dirname(path.resolve(sourceArg));\n }\n return process.cwd();\n}\nexport function compareCommand(): Command {\n const cmd = new Command('compare');\n cmd\n .description(\n 'Compare two schemas (the desired state vs the current state). Each side may be a .ddtproj, a .ddtpac, or databricks://<profile>[/catalog[/schema]].',\n )\n .requiredOption(\n '--source <ref>',\n 'Source / desired state: .ddtproj, .ddtpac, or databricks://<profile>[/catalog[/schema]].',\n )\n .requiredOption(\n '--target <ref>',\n 'Target / current state: .ddtproj, .ddtpac, or databricks://<profile>[/catalog[/schema]].',\n )\n .option('--ignore-case', 'Compare object FQNs case-insensitively.', false)\n .option(\n '--json',\n 'Emit a JSON CompareResult instead of human-readable output. (Alias for --format json.)',\n false,\n )\n .option(\n '--format <fmt>',\n 'Output format: summary | json | markdown. Default summary.',\n 'summary',\n )\n .option('--report-html <path>', 'Also write a self-contained HTML compare-report to <path>.')\n .option(\n '--no-slice',\n \"Disable the source pac's Project Slice (if it has one). Default: a sliced pac is partitioned automatically.\",\n )\n .option(\n '--explain',\n 'After the diff, call the configured AI provider to narrate each change in plain English with reasoning. Requires `ddt ai` to be configured.',\n false,\n )\n .option(\n '--color <mode>',\n 'Colorize severity output: always | never | auto. Default auto (color on TTY).',\n 'auto',\n )\n .option(\n '--type-safe',\n 'After the compare, run the TYPECHECK.1 impact analyzer. Exits with code 2 if the configured --break-on threshold is reached (default `error`). CI-friendly gate — pair with `--format json` for machine-readable output.',\n false,\n )\n .option(\n '--break-on <severity>',\n 'TYPECHECK.2 threshold for --type-safe: `error` (exit 2 on any error ripple, default) | `warning` (exit 2 on any error OR warning ripple, strict CI mode).',\n 'error',\n )\n .option(\n '--write-impact [path]',\n 'Write the TYPECHECK.1 impact analysis to a JSON file the VS Code extension can surface as squiggle-underlines. Default path: `.ddt/impact.json` resolved relative to CWD. Composes with `--type-safe` — the file is written before the gate decides exit code.',\n )\n .option(\n '--no-history',\n 'Skip writing the compare-history audit record (AUDITCMP.1). Default: every compare run writes a record to `.ddt/history/compare/`, exportable via `ddt audit-log emit`.',\n );\n addMappingFlags(cmd);\n cmd.action(async (opts) => {\n const nameMapping = await buildMappingFromOptions(opts);\n const engine = new CompareEngine();\n const source = await resolveSource(String(opts.source));\n const target = await resolveSource(String(opts.target));\n\n // Auto-load the source's Project Slice (from a `.ddtproj` file or a\n // `.ddtpac` manifest) unless --no-slice was set. Live sources have no slice.\n const slice =\n opts.slice === false ? undefined : await maybeLoadSliceFromSource(String(opts.source));\n\n const startedAt = new Date().toISOString();\n const result = await engine.compare(source, target, {\n ignoreCase: !!opts.ignoreCase,\n ...(nameMapping ? { nameMapping } : {}),\n ...(slice ? { sliceFilter: slice } : {}),\n });\n\n // AUDITCMP.1 — compare operations leave the same audit trail deploys\n // do. Written before the format-specific early returns so every\n // output mode is covered. Best-effort: a failed write never breaks\n // the compare itself.\n if (opts.history !== false) {\n const finishedAt = new Date().toISOString();\n try {\n const histAssessment = safety.assess(result);\n const changed = result.summary.added + result.summary.removed + result.summary.modified;\n await writeCompareHistory(compareHistoryRoot(String(opts.source)), {\n startedAt,\n finishedAt,\n durationMs: Date.parse(finishedAt) - Date.parse(startedAt),\n outcome: histAssessment.blocked\n ? 'BLOCKED'\n : changed === 0\n ? 'NO_CHANGES'\n : 'CHANGES_FOUND',\n source: { kind: result.source.kind, label: result.source.label },\n target: { kind: result.target.kind, label: result.target.label },\n summary: result.summary,\n sliceApplied: Boolean(slice),\n ...(histAssessment.blocked && histAssessment.blockReason\n ? { blockReason: histAssessment.blockReason }\n : {}),\n });\n } catch {\n /* audit trail is best-effort; never fail the compare over it */\n }\n }\n\n if (opts.reportHtml) {\n const html = renderHtmlReport(result, {\n title: `Compare ${result.source.label} → ${result.target.label}`,\n safety: safety.assess(result),\n });\n const htmlPath = path.resolve(String(opts.reportHtml));\n await fs.mkdir(path.dirname(htmlPath), { recursive: true });\n await fs.writeFile(htmlPath, html, 'utf8');\n console.error(`Wrote ${htmlPath} (${html.length} bytes).`);\n }\n\n const fmt = opts.json ? 'json' : String(opts.format ?? 'summary').toLowerCase();\n if (fmt === 'json') {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n if (fmt === 'markdown') {\n const assessment = safety.assess(result);\n console.log(renderCompareMarkdown(result, assessment));\n return;\n }\n const s = result.summary;\n console.log(`Source: ${result.source.kind}:${result.source.label}`);\n console.log(`Target: ${result.target.kind}:${result.target.label}`);\n const mappingSummary = describeMapping(nameMapping);\n if (mappingSummary) console.log(`Mapping: ${mappingSummary}`);\n if (slice) {\n const outside = result.outsideScope?.length ?? 0;\n const refs = result.referenced?.length ?? 0;\n console.log(\n `Slice active: ${result.objects.length} owned · ${outside} outside scope (untouched) · ${refs} referenced`,\n );\n }\n console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);\n for (const o of result.objects) {\n if (o.kind === 'unchanged') continue;\n const glyph = o.kind === 'added' ? '+' : o.kind === 'removed' ? '-' : '~';\n console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);\n if (o.kind === 'modified' && o.changes) {\n for (const c of o.changes) {\n console.log(\n ` • ${c.path}: ${JSON.stringify(c.target)} → ${JSON.stringify(c.source)}`,\n );\n }\n }\n }\n\n // Reversibility-grouped safety findings — Unrecoverable first (highest\n // urgency), then Data-impacting, then Reversible. Empty buckets are\n // skipped, and an empty assessment is silent.\n const assessment = safety.assess(result);\n const buckets = safety.groupByReversibility(assessment);\n const total =\n buckets.unrecoverable.length + buckets.dataImpacting.length + buckets.reversible.length;\n if (total > 0) {\n const colorize = makeColorizer(resolveColorMode(opts.color));\n console.log('');\n console.log('Safety findings (grouped by reversibility):');\n const block = safety.formatReversibilityBuckets(buckets);\n const colored = colorize.applyToBlock(block);\n for (const line of colored.split('\\n')) console.log(' ' + line);\n if (assessment.blocked && assessment.blockReason) {\n console.error(' ' + colorize.unrecoverable('BLOCKED: ' + assessment.blockReason));\n }\n }\n\n // AI-narrated diff (--explain). Calls the configured provider with a\n // structured prompt containing the diff summary + per-object changes\n // + safety findings.\n if (opts.explain) {\n console.log('');\n console.log('AI explanation:');\n try {\n const userPrompt = buildExplainPrompt(result, assessment);\n const reply = await ai.complete(\n [\n { role: 'system', content: SYSTEM_PROMPT },\n { role: 'user', content: userPrompt },\n ],\n { feature: 'compare.explain' },\n );\n for (const line of reply.text.split('\\n')) console.log(' ' + line);\n } catch (err) {\n console.error(' --explain failed: ' + (err instanceof Error ? err.message : String(err)));\n console.error(\n ' Run `ddt ai status` to verify your AI provider is configured (`ddt ai test` to send a probe).',\n );\n }\n }\n\n // --write-impact: TYPECHECK.4 — write the impact analysis to a\n // JSON file the VS Code provider reads. Always runs the analyzer\n // (even when --type-safe is off) so the editor surface is\n // independent of the CI-gate flag. Composes with --type-safe.\n if (opts.writeImpact !== undefined) {\n await writeImpactJson(\n result,\n opts.writeImpact === true ? undefined : String(opts.writeImpact),\n String(opts.source),\n );\n }\n\n // --type-safe: TYPECHECK.2 CI gate. Runs the TYPECHECK.1 impact\n // analyzer and exits with code 2 if any error-severity ripple\n // (column-drop or table-drop reaching a dependent object) is\n // detected. Stays silent on a clean diff so this composes well\n // with --format json (which already returned above).\n if (opts.typeSafe) {\n const breakOnRaw = String(opts.breakOn ?? 'error').toLowerCase();\n if (breakOnRaw !== 'error' && breakOnRaw !== 'warning') {\n throw new Error(\n `--break-on must be 'error' or 'warning' (got '${opts.breakOn}'). 'error' fails on any error-severity ripple (default); 'warning' fails on any warning or error ripple (strict CI).`,\n );\n }\n const breakOn = breakOnRaw as 'error' | 'warning';\n const impactFindings = typecheck.analyzeImpact(result);\n const impactSummary = typecheck.summarizeImpact(impactFindings);\n if (impactSummary.findingsCount > 0) {\n console.log('');\n console.log(\n `Type-safety gate (--break-on ${breakOn}) — ${impactSummary.findingsCount} breaking change(s); ${impactSummary.errors} error ripple(s), ${impactSummary.warnings} warning ripple(s); ${impactSummary.affectedObjects.length} dependent object(s).`,\n );\n for (const f of impactFindings) {\n for (const r of f.ripples) {\n const tag = r.severity === 'error' ? 'ERROR' : 'WARN ';\n console.log(\n ` ${tag} ${r.fqn} ← ${f.breakingChangeOn.fqn} (${f.breakingChangeOn.kind})`,\n );\n }\n }\n const failOnError = impactSummary.errors > 0;\n const failOnWarning = breakOn === 'warning' && impactSummary.warnings > 0;\n if (failOnError) {\n console.error(\n `Type-safety gate FAILED — ${impactSummary.errors} error ripple(s). Fix the dependents (drop / update / re-bind) before re-running compare, OR rerun with the change scoped out of the diff.`,\n );\n process.exitCode = 2;\n } else if (failOnWarning) {\n console.error(\n `Type-safety gate FAILED (strict --break-on warning) — ${impactSummary.warnings} warning ripple(s). Tighten the dependent SQL to accept the new type, or drop --break-on warning to allow.`,\n );\n process.exitCode = 2;\n }\n } else {\n console.log('Type-safety gate PASSED — no breaking-change ripples detected.');\n }\n }\n });\n attachRelatedOptions(cmd, [\n 'compare.ignoreCase',\n 'compare.ignoreComments',\n 'compare.ignoreFormattingDifferences',\n 'compare.ignoreClusterBy',\n 'compare.ignorePartitioning',\n 'compare.ignoreStorageLocation',\n ]);\n return cmd;\n}\n\n/**\n * TYPECHECK.4 — write the impact analysis JSON to disk so the VS Code\n * provider can surface it as squiggle-underlines. Defaults to\n * `<project-dir>/.ddt/impact.json` when the source is a `.ddtproj` / `.ddtpac`,\n * otherwise `<cwd>/.ddt/impact.json` (live `databricks://` sources). Mirrors\n * SDT's `defaultImpactPath`.\n */\nasync function writeImpactJson(\n result: Parameters<typeof typecheck.analyzeImpact>[0],\n explicitPath: string | undefined,\n sourceArg: string,\n): Promise<void> {\n const findings = typecheck.analyzeImpact(result);\n const summary = typecheck.summarizeImpact(findings);\n const file: typecheck.ImpactFile = {\n version: typecheck.IMPACT_FILE_VERSION,\n generatedAt: new Date().toISOString(),\n source: `${result.source.kind}:${result.source.label}`,\n target: `${result.target.kind}:${result.target.label}`,\n findings,\n summary,\n };\n const outPath = explicitPath ? path.resolve(explicitPath) : defaultImpactPath(sourceArg);\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, typecheck.serializeImpactFile(file));\n console.log(\n `Wrote ${outPath} — ${summary.findingsCount} finding(s), ${summary.errors} error / ${summary.warnings} warning ripple(s).`,\n );\n}\n\nfunction defaultImpactPath(sourceArg: string): string {\n const lower = sourceArg.toLowerCase();\n if (lower.endsWith('.ddtproj') || lower.endsWith('.ddtpac')) {\n return path.resolve(path.dirname(sourceArg), '.ddt', 'impact.json');\n }\n return path.resolve('.ddt', 'impact.json');\n}\n\nexport const SYSTEM_PROMPT = `You are a senior Databricks Unity Catalog DBA reviewing a schema diff. Your job is to narrate the changes in plain English to a teammate who hasn't seen the underlying SQL. For each change, briefly say:\n - what it is (e.g. \"a new fact table is added\")\n - why a reasonable engineer might do this (the intent)\n - what to watch out for (the risk, if any)\n\nBe concrete; use the FQNs you're given. Keep it to 3-6 sentences per object. Don't repeat the raw diff back — explain it.`;\n\nexport function buildExplainPrompt(\n result: {\n source: { label: string };\n target: { label: string };\n objects: Array<{\n kind: string;\n identity: { fqn: string; objectType: string };\n changes?: unknown[];\n }>;\n summary: { added: number; removed: number; modified: number; unchanged: number };\n },\n assessment: {\n unrecoverable: unknown[];\n destructive: unknown[];\n expensive: unknown[];\n warnings: unknown[];\n },\n): string {\n const lines: string[] = [];\n lines.push(`Source: ${result.source.label}`);\n lines.push(`Target: ${result.target.label}`);\n lines.push(\n `Summary: +${result.summary.added} added, -${result.summary.removed} removed, ~${result.summary.modified} modified.`,\n );\n lines.push(\n `Safety: ${assessment.unrecoverable.length} unrecoverable, ${assessment.destructive.length} destructive, ${assessment.expensive.length} expensive, ${assessment.warnings.length} warnings.`,\n );\n lines.push('');\n lines.push('Changes (up to 40):');\n const changed = result.objects.filter((o) => o.kind !== 'unchanged').slice(0, 40);\n for (const o of changed) {\n lines.push(`- ${o.kind} ${o.identity.objectType} ${o.identity.fqn}`);\n }\n if (result.objects.filter((o) => o.kind !== 'unchanged').length > 40) {\n lines.push(\n ` (… ${result.objects.filter((o) => o.kind !== 'unchanged').length - 40} more truncated)`,\n );\n }\n lines.push('');\n lines.push(\n 'Please narrate this diff in plain English. Group related changes together when it helps.',\n );\n return lines.join('\\n');\n}\n\nfunction renderCompareMarkdown(\n result: {\n source: { kind: string; label: string };\n target: { kind: string; label: string };\n objects: Array<{ kind: string; identity: { fqn: string; objectType: string } }>;\n summary: { added: number; removed: number; modified: number; unchanged: number };\n },\n assessment: {\n unrecoverable: unknown[];\n destructive: unknown[];\n expensive: unknown[];\n warnings: unknown[];\n blocked?: boolean;\n blockReason?: string;\n },\n): string {\n const lines: string[] = [];\n lines.push(\n `# Compare: ${result.source.kind}:${result.source.label} → ${result.target.kind}:${result.target.label}`,\n );\n lines.push('');\n lines.push(\n `**Summary**: +${result.summary.added} added · -${result.summary.removed} removed · ~${result.summary.modified} modified · =${result.summary.unchanged} unchanged.`,\n );\n lines.push('');\n lines.push('**Safety**:');\n lines.push(`- 🛑 Unrecoverable: ${assessment.unrecoverable.length}`);\n lines.push(`- 🔥 Destructive: ${assessment.destructive.length}`);\n lines.push(`- ⏱ Expensive: ${assessment.expensive.length}`);\n lines.push(`- ⚠ Warnings: ${assessment.warnings.length}`);\n if (assessment.blocked) {\n lines.push('');\n lines.push(\n `> **BLOCKED**: ${assessment.blockReason ?? 'safety classifier refuses to proceed'}`,\n );\n }\n lines.push('');\n const changed = result.objects.filter((o) => o.kind !== 'unchanged');\n if (changed.length === 0) {\n lines.push('_No object-level changes._');\n return lines.join('\\n');\n }\n lines.push('| Kind | Type | FQN |');\n lines.push('| --- | --- | --- |');\n for (const o of changed) {\n lines.push(`| ${o.kind} | \\`${o.identity.objectType}\\` | \\`${o.identity.fqn}\\` |`);\n }\n return lines.join('\\n');\n}\n","/**\n * ANSI severity coloring for CLI output, DDT side. Paired with\n * `Snowflake/packages/cli/src/util/color.ts` byte-for-byte at the\n * API level.\n */\nexport type ColorMode = 'always' | 'never' | 'auto';\n\nconst ANSI = {\n reset: '\\x1b[0m',\n bold: '\\x1b[1m',\n red: '\\x1b[31m',\n yellow: '\\x1b[33m',\n cyan: '\\x1b[36m',\n green: '\\x1b[32m',\n gray: '\\x1b[90m',\n};\n\nexport function resolveColorMode(flag: string | undefined): boolean {\n const mode = (flag ?? 'auto').toLowerCase() as ColorMode;\n if (mode === 'always') return true;\n if (mode === 'never') return false;\n if (process.env.NO_COLOR) return false;\n return Boolean(process.stdout.isTTY);\n}\n\nexport interface Colorizer {\n unrecoverable(s: string): string;\n destructive(s: string): string;\n expensive(s: string): string;\n warning(s: string): string;\n safe(s: string): string;\n dim(s: string): string;\n applyToBlock(block: string): string;\n}\n\nexport function makeColorizer(enabled: boolean): Colorizer {\n if (!enabled) {\n const identity = (s: string): string => s;\n return {\n unrecoverable: identity,\n destructive: identity,\n expensive: identity,\n warning: identity,\n safe: identity,\n dim: identity,\n applyToBlock: identity,\n };\n }\n const wrap =\n (codes: string) =>\n (s: string): string =>\n `${codes}${s}${ANSI.reset}`;\n return {\n unrecoverable: wrap(ANSI.bold + ANSI.red),\n destructive: wrap(ANSI.red),\n expensive: wrap(ANSI.yellow),\n warning: wrap(ANSI.cyan),\n safe: wrap(ANSI.green),\n dim: wrap(ANSI.gray),\n applyToBlock(block: string): string {\n return block\n .split('\\n')\n .map((line) => {\n if (/UNRECOVERABLE/.test(line) || line.includes('🛑'))\n return ANSI.bold + ANSI.red + line + ANSI.reset;\n if (/DESTRUCTIVE/.test(line)) return ANSI.red + line + ANSI.reset;\n if (/EXPENSIVE/.test(line)) return ANSI.yellow + line + ANSI.reset;\n if (/WARNING/.test(line) || line.includes('⚠')) return ANSI.cyan + line + ANSI.reset;\n if (/\\bSAFE\\b|\\bOK\\b/.test(line) || line.includes('✓'))\n return ANSI.green + line + ANSI.reset;\n return line;\n })\n .join('\\n');\n },\n };\n}\n"],"mappings":"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","names":["assessment"]}
|
|
@@ -60,7 +60,7 @@ function teardownErrorReporting() {
|
|
|
60
60
|
function activeConsentForTests() {
|
|
61
61
|
return activeConsent;
|
|
62
62
|
}
|
|
63
|
-
var BETA_VERSION = "0.2.
|
|
63
|
+
var BETA_VERSION = "0.2.5";
|
|
64
64
|
function shouldPromptFirstRun(consent) {
|
|
65
65
|
return consent === "unset";
|
|
66
66
|
}
|
|
@@ -106,4 +106,4 @@ export {
|
|
|
106
106
|
shouldPromptFirstRun,
|
|
107
107
|
teardownErrorReporting
|
|
108
108
|
};
|
|
109
|
-
//# sourceMappingURL=errorReporting-
|
|
109
|
+
//# sourceMappingURL=errorReporting-LX6WT4JH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/util/errorReporting.ts"],"sourcesContent":["/**\n * CLI error-reporting wiring (ERR.2).\n *\n * Lazily imported from `cli.ts` hooks so the cold-start path pays zero cost\n * until a command actually runs (preAction) or fails (catch handler).\n *\n * Responsibilities:\n * - first-run consent prompt (TTY only, default Yes, one keystroke)\n * - crash-hook installation when consent allows capture\n * - end-of-run drain: spool → `POST /errors` when consent is `on`\n * - manual \"Report this error? [y/N]\" prompt when consent is NOT `on`\n * and a command fails\n *\n * @see @ddt-tools/core/errorReport (capture substrate + consent + transport)\n */\nimport { createInterface } from 'node:readline';\n// Subpath import, NOT the `@ddt-tools/core` barrel — this module runs in the\n// preAction hook of every command; the barrel would drag ~85 core modules in.\nimport * as errorReport from '@ddt-tools/core/errorReport';\nimport { logger } from './logger.js';\n\n/** Commands that must never trigger the consent prompt or auto-drain. */\nconst EXEMPT_COMMANDS = new Set(['telemetry', 'feedback', 'help', 'completion']);\n\nlet activeConsent: errorReport.ErrorReportConsent = 'unset';\nlet uninstallHooks: (() => void) | null = null;\n\n/**\n * preAction hook body. Reads (and on first run, prompts for) consent, then\n * installs crash capture unless the user opted out.\n */\nexport async function setupErrorReporting(commandName: string): Promise<void> {\n if (EXEMPT_COMMANDS.has(commandName)) return;\n const stored = await errorReport.readConsent();\n activeConsent = stored.consent;\n\n // First-run consent prompt — explicit, one keystroke, default Yes.\n // Skipped when not interactive (CI, pipes) so scripted runs never block.\n if (\n shouldPromptFirstRun(stored.consent) &&\n process.stdout.isTTY &&\n process.stdin.isTTY &&\n !isCi()\n ) {\n // Beta install-time messaging — shown ONCE, immediately before the very\n // first consent question (true first run, consent still `unset`).\n printBetaNotice();\n activeConsent = (await promptFirstRunConsent()) ? 'on' : 'off';\n await errorReport.writeConsent(activeConsent === 'on' ? 'on' : 'off');\n }\n\n // Crash capture is installed unless the user said no. Capture is local-only;\n // nothing leaves the machine without `isErrorReportingEnabled` saying so.\n if (activeConsent !== 'off') {\n uninstallHooks = errorReport.installProcessHooks();\n }\n}\n\n/**\n * postAction hook body. Flushes buffered events and, when consent is `on`,\n * drains the spool to the Worker. No-ops fast when there is nothing to send.\n * `transport` is injectable for tests.\n */\nexport async function finishErrorReporting(\n productVersion: string,\n transport: errorReport.TransportOptions = {},\n): Promise<void> {\n await errorReport.flushErrorEvents();\n\n // Anonymous usage ping (opt-in) — gated internally by the SAME consent +\n // env opt-outs as error reporting and throttled to once per 24h. It is\n // awaited here (not detached) so the postAction lifecycle has a\n // deterministic completion point: `sendUsagePing` is self-bounded (3s hard\n // timeout) and never throws, so awaiting it can block command exit by at\n // most that timeout — the same bound the spool drain below already imposes.\n // Detaching it left the send racing process teardown, which both dropped\n // pings in practice and made the wiring untestable without a sleep.\n // `transport.fetchImpl` is threaded through so tests can intercept it.\n await errorReport\n .sendUsagePing({\n product: 'ddt',\n version: productVersion,\n surface: 'cli',\n ...(transport.fetchImpl ? { fetchImpl: transport.fetchImpl } : {}),\n })\n .catch(() => undefined);\n\n if (!errorReport.isErrorReportingEnabled(activeConsent)) return;\n const spooled = await errorReport.listSpooled(transport.dir);\n if (spooled.length === 0) return;\n const result = await errorReport.drainSpool({ productVersion }, transport);\n if (result.sent > 0) {\n logger.dim(` (${result.sent} error report${result.sent === 1 ? '' : 's'} sent — thank you)`);\n }\n}\n\n/**\n * Top-level command-failure handler. Reports the error as `handled` (real\n * crashes go through `uncaughtExceptionMonitor`), then either auto-sends\n * (consent `on`) or offers a one-keystroke manual report.\n * `transport` is injectable for tests.\n */\nexport async function reportCliFailure(\n err: unknown,\n productVersion: string,\n transport: errorReport.TransportOptions = {},\n): Promise<void> {\n errorReport.reportError(err, 'handled', 'cli:main');\n await errorReport.flushErrorEvents(transport.dir);\n\n if (errorReport.isErrorReportingEnabled(activeConsent)) {\n await errorReport.drainSpool({ productVersion }, transport);\n return;\n }\n\n // Manual push path — only when interactive.\n if (!process.stdout.isTTY || !process.stdin.isTTY || isCi()) return;\n const yes = await promptYesNo('Report this error to the DDT team? [y/N] ', false);\n if (!yes) return;\n const result = await errorReport.drainSpool({ productVersion }, transport);\n if (result.sent > 0) logger.dim(' Report sent — thank you.');\n else logger.dim(' Could not reach the error endpoint; the report is queued locally.');\n}\n\n/** Uninstall crash hooks (tests). */\nexport function teardownErrorReporting(): void {\n uninstallHooks?.();\n uninstallHooks = null;\n activeConsent = 'unset';\n}\n\n/** Current consent as seen by the hooks (tests). */\nexport function activeConsentForTests(): errorReport.ErrorReportConsent {\n return activeConsent;\n}\n\n/** DDT version surfaced in the first-run beta notice. */\nconst BETA_VERSION = '0.2.4';\n\n/**\n * Whether the first-run flow (beta notice + consent prompt) should fire.\n * True ONLY on true first run — when consent has never been decided\n * (`unset`). Exported so the \"first run only\" contract is unit-testable\n * without faking a TTY.\n */\nexport function shouldPromptFirstRun(consent: errorReport.ErrorReportConsent): boolean {\n return consent === 'unset';\n}\n\n/**\n * Beta install-time messaging — printed ONCE, on true first run (consent\n * `unset`), immediately before the consent question. Tells the user, at\n * install time: it's a 30-day public beta with all features free; what\n * happens after the beta (core stays free forever, Pro features keep\n * working but show license notices); that AI features are bring-your-own\n * key; and how to report a bug. Plain ASCII box to match CLI output style.\n */\nexport function printBetaNotice(): void {\n const lines = [\n '┌─────────────────────────────────────────────────────────┐',\n `│ DDT ${BETA_VERSION} — Public Beta │`,\n '│ • All features are free during the beta. │',\n '│ • After the beta: core features stay free forever; │',\n '│ Pro features keep working and show license notices. │',\n '│ • AI features use your own API key (never ours). │',\n '│ • Found a bug? Run: ddt feedback \"<what happened>\" │',\n '└─────────────────────────────────────────────────────────┘',\n ];\n for (const line of lines) logger.info(line);\n}\n\nasync function promptFirstRunConsent(): Promise<boolean> {\n logger.info('DDT can report errors automatically (sanitized diagnostics + OS context,');\n logger.info('never your SQL, identifiers, or credentials) so they get fixed fast.');\n logger.info(errorReport.CONSENT_WARNING);\n return promptYesNo('Enable automatic error reporting? [Y/n] ', true);\n}\n\nfunction promptYesNo(question: string, defaultYes: boolean): Promise<boolean> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer) => {\n rl.close();\n const normalized = answer.trim().toLowerCase();\n if (normalized === '') resolve(defaultYes);\n else resolve(normalized === 'y' || normalized === 'yes');\n });\n });\n}\n\nfunction isCi(): boolean {\n return process.env['CI'] === 'true';\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/util/errorReporting.ts"],"sourcesContent":["/**\n * CLI error-reporting wiring (ERR.2).\n *\n * Lazily imported from `cli.ts` hooks so the cold-start path pays zero cost\n * until a command actually runs (preAction) or fails (catch handler).\n *\n * Responsibilities:\n * - first-run consent prompt (TTY only, default Yes, one keystroke)\n * - crash-hook installation when consent allows capture\n * - end-of-run drain: spool → `POST /errors` when consent is `on`\n * - manual \"Report this error? [y/N]\" prompt when consent is NOT `on`\n * and a command fails\n *\n * @see @ddt-tools/core/errorReport (capture substrate + consent + transport)\n */\nimport { createInterface } from 'node:readline';\n// Subpath import, NOT the `@ddt-tools/core` barrel — this module runs in the\n// preAction hook of every command; the barrel would drag ~85 core modules in.\nimport * as errorReport from '@ddt-tools/core/errorReport';\nimport { logger } from './logger.js';\n\n/** Commands that must never trigger the consent prompt or auto-drain. */\nconst EXEMPT_COMMANDS = new Set(['telemetry', 'feedback', 'help', 'completion']);\n\nlet activeConsent: errorReport.ErrorReportConsent = 'unset';\nlet uninstallHooks: (() => void) | null = null;\n\n/**\n * preAction hook body. Reads (and on first run, prompts for) consent, then\n * installs crash capture unless the user opted out.\n */\nexport async function setupErrorReporting(commandName: string): Promise<void> {\n if (EXEMPT_COMMANDS.has(commandName)) return;\n const stored = await errorReport.readConsent();\n activeConsent = stored.consent;\n\n // First-run consent prompt — explicit, one keystroke, default Yes.\n // Skipped when not interactive (CI, pipes) so scripted runs never block.\n if (\n shouldPromptFirstRun(stored.consent) &&\n process.stdout.isTTY &&\n process.stdin.isTTY &&\n !isCi()\n ) {\n // Beta install-time messaging — shown ONCE, immediately before the very\n // first consent question (true first run, consent still `unset`).\n printBetaNotice();\n activeConsent = (await promptFirstRunConsent()) ? 'on' : 'off';\n await errorReport.writeConsent(activeConsent === 'on' ? 'on' : 'off');\n }\n\n // Crash capture is installed unless the user said no. Capture is local-only;\n // nothing leaves the machine without `isErrorReportingEnabled` saying so.\n if (activeConsent !== 'off') {\n uninstallHooks = errorReport.installProcessHooks();\n }\n}\n\n/**\n * postAction hook body. Flushes buffered events and, when consent is `on`,\n * drains the spool to the Worker. No-ops fast when there is nothing to send.\n * `transport` is injectable for tests.\n */\nexport async function finishErrorReporting(\n productVersion: string,\n transport: errorReport.TransportOptions = {},\n): Promise<void> {\n await errorReport.flushErrorEvents();\n\n // Anonymous usage ping (opt-in) — gated internally by the SAME consent +\n // env opt-outs as error reporting and throttled to once per 24h. It is\n // awaited here (not detached) so the postAction lifecycle has a\n // deterministic completion point: `sendUsagePing` is self-bounded (3s hard\n // timeout) and never throws, so awaiting it can block command exit by at\n // most that timeout — the same bound the spool drain below already imposes.\n // Detaching it left the send racing process teardown, which both dropped\n // pings in practice and made the wiring untestable without a sleep.\n // `transport.fetchImpl` is threaded through so tests can intercept it.\n await errorReport\n .sendUsagePing({\n product: 'ddt',\n version: productVersion,\n surface: 'cli',\n ...(transport.fetchImpl ? { fetchImpl: transport.fetchImpl } : {}),\n })\n .catch(() => undefined);\n\n if (!errorReport.isErrorReportingEnabled(activeConsent)) return;\n const spooled = await errorReport.listSpooled(transport.dir);\n if (spooled.length === 0) return;\n const result = await errorReport.drainSpool({ productVersion }, transport);\n if (result.sent > 0) {\n logger.dim(` (${result.sent} error report${result.sent === 1 ? '' : 's'} sent — thank you)`);\n }\n}\n\n/**\n * Top-level command-failure handler. Reports the error as `handled` (real\n * crashes go through `uncaughtExceptionMonitor`), then either auto-sends\n * (consent `on`) or offers a one-keystroke manual report.\n * `transport` is injectable for tests.\n */\nexport async function reportCliFailure(\n err: unknown,\n productVersion: string,\n transport: errorReport.TransportOptions = {},\n): Promise<void> {\n errorReport.reportError(err, 'handled', 'cli:main');\n await errorReport.flushErrorEvents(transport.dir);\n\n if (errorReport.isErrorReportingEnabled(activeConsent)) {\n await errorReport.drainSpool({ productVersion }, transport);\n return;\n }\n\n // Manual push path — only when interactive.\n if (!process.stdout.isTTY || !process.stdin.isTTY || isCi()) return;\n const yes = await promptYesNo('Report this error to the DDT team? [y/N] ', false);\n if (!yes) return;\n const result = await errorReport.drainSpool({ productVersion }, transport);\n if (result.sent > 0) logger.dim(' Report sent — thank you.');\n else logger.dim(' Could not reach the error endpoint; the report is queued locally.');\n}\n\n/** Uninstall crash hooks (tests). */\nexport function teardownErrorReporting(): void {\n uninstallHooks?.();\n uninstallHooks = null;\n activeConsent = 'unset';\n}\n\n/** Current consent as seen by the hooks (tests). */\nexport function activeConsentForTests(): errorReport.ErrorReportConsent {\n return activeConsent;\n}\n\n/** DDT version surfaced in the first-run beta notice. */\nconst BETA_VERSION = '0.2.5';\n\n/**\n * Whether the first-run flow (beta notice + consent prompt) should fire.\n * True ONLY on true first run — when consent has never been decided\n * (`unset`). Exported so the \"first run only\" contract is unit-testable\n * without faking a TTY.\n */\nexport function shouldPromptFirstRun(consent: errorReport.ErrorReportConsent): boolean {\n return consent === 'unset';\n}\n\n/**\n * Beta install-time messaging — printed ONCE, on true first run (consent\n * `unset`), immediately before the consent question. Tells the user, at\n * install time: it's a 30-day public beta with all features free; what\n * happens after the beta (core stays free forever, Pro features keep\n * working but show license notices); that AI features are bring-your-own\n * key; and how to report a bug. Plain ASCII box to match CLI output style.\n */\nexport function printBetaNotice(): void {\n const lines = [\n '┌─────────────────────────────────────────────────────────┐',\n `│ DDT ${BETA_VERSION} — Public Beta │`,\n '│ • All features are free during the beta. │',\n '│ • After the beta: core features stay free forever; │',\n '│ Pro features keep working and show license notices. │',\n '│ • AI features use your own API key (never ours). │',\n '│ • Found a bug? Run: ddt feedback \"<what happened>\" │',\n '└─────────────────────────────────────────────────────────┘',\n ];\n for (const line of lines) logger.info(line);\n}\n\nasync function promptFirstRunConsent(): Promise<boolean> {\n logger.info('DDT can report errors automatically (sanitized diagnostics + OS context,');\n logger.info('never your SQL, identifiers, or credentials) so they get fixed fast.');\n logger.info(errorReport.CONSENT_WARNING);\n return promptYesNo('Enable automatic error reporting? [Y/n] ', true);\n}\n\nfunction promptYesNo(question: string, defaultYes: boolean): Promise<boolean> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer) => {\n rl.close();\n const normalized = answer.trim().toLowerCase();\n if (normalized === '') resolve(defaultYes);\n else resolve(normalized === 'y' || normalized === 'yes');\n });\n });\n}\n\nfunction isCi(): boolean {\n return process.env['CI'] === 'true';\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/import.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { importers as importersApi } from "@ddt-tools/core";
|
|
6
|
+
function importCommand() {
|
|
7
|
+
return new Command("import").description("Convert artifacts from other tools into a DDT project.").requiredOption(
|
|
8
|
+
"--from <source>",
|
|
9
|
+
"Source format (liquibase, flyway, dbt, terraform-state, sql-files, cross-engine-ddl, databricks-workspace)"
|
|
10
|
+
).requiredOption("--source-path <path>", "Source artifact path (file or directory)").requiredOption("--output <dir>", "Output directory for the generated DDT project").option("--connection <profile>", "Connection profile (required for databricks-workspace)").action(
|
|
11
|
+
async (opts) => {
|
|
12
|
+
const source = opts.from;
|
|
13
|
+
const importer = importersApi.getImporter(source);
|
|
14
|
+
const result = await importer.import({
|
|
15
|
+
sourcePath: opts.sourcePath,
|
|
16
|
+
outputDir: opts.output,
|
|
17
|
+
connectionProfile: opts.connection
|
|
18
|
+
});
|
|
19
|
+
console.log(
|
|
20
|
+
`Imported ${result.filesCreated} files. Project written to ${result.projectPath}`
|
|
21
|
+
);
|
|
22
|
+
for (const w of result.warnings) console.warn(w);
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
importCommand
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=import-EGOVKTLX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/import.ts"],"sourcesContent":["/**\n * `ddt import` — convert artifacts from other tools into a `.ddtproj`.\n *\n * Sources (per the DDT importer registry):\n * - liquibase (changelog XML/YAML/SQL)\n * - flyway (versioned / repeatable migration tree)\n * - dbt (compiled manifest.json / project)\n * - terraform-state (`.tfstate` from the Databricks provider)\n * - sql-files (flat dir of `.sql`)\n * - cross-engine-ddl (foreign-dialect DDL, best-effort translation)\n * - databricks-workspace (live workspace → project, needs --connection)\n *\n * To split a single SQL script into an existing project tree instead, use\n * `ddt import-script`. Byte-aligned with `sdt import`.\n */\nimport { Command } from 'commander';\n\nimport { importers as importersApi } from '@ddt-tools/core';\n\nexport function importCommand(): Command {\n return new Command('import')\n .description('Convert artifacts from other tools into a DDT project.')\n .requiredOption(\n '--from <source>',\n 'Source format (liquibase, flyway, dbt, terraform-state, sql-files, cross-engine-ddl, databricks-workspace)',\n )\n .requiredOption('--source-path <path>', 'Source artifact path (file or directory)')\n .requiredOption('--output <dir>', 'Output directory for the generated DDT project')\n .option('--connection <profile>', 'Connection profile (required for databricks-workspace)')\n .action(\n async (opts: { from: string; sourcePath: string; output: string; connection?: string }) => {\n const source = opts.from as Parameters<typeof importersApi.getImporter>[0];\n const importer = importersApi.getImporter(source);\n const result = await importer.import({\n sourcePath: opts.sourcePath,\n outputDir: opts.output,\n connectionProfile: opts.connection,\n });\n console.log(\n `Imported ${result.filesCreated} files. Project written to ${result.projectPath}`,\n );\n for (const w of result.warnings) console.warn(w);\n },\n );\n}\n"],"mappings":";;;AAeA,SAAS,eAAe;AAExB,SAAS,aAAa,oBAAoB;AAEnC,SAAS,gBAAyB;AACvC,SAAO,IAAI,QAAQ,QAAQ,EACxB,YAAY,wDAAwD,EACpE;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,eAAe,wBAAwB,0CAA0C,EACjF,eAAe,kBAAkB,gDAAgD,EACjF,OAAO,0BAA0B,wDAAwD,EACzF;AAAA,IACC,OAAO,SAAoF;AACzF,YAAM,SAAS,KAAK;AACpB,YAAM,WAAW,aAAa,YAAY,MAAM;AAChD,YAAM,SAAS,MAAM,SAAS,OAAO;AAAA,QACnC,YAAY,KAAK;AAAA,QACjB,WAAW,KAAK;AAAA,QAChB,mBAAmB,KAAK;AAAA,MAC1B,CAAC;AACD,cAAQ;AAAA,QACN,YAAY,OAAO,YAAY,8BAA8B,OAAO,WAAW;AAAA,MACjF;AACA,iBAAW,KAAK,OAAO,SAAU,SAAQ,KAAK,CAAC;AAAA,IACjD;AAAA,EACF;AACJ;","names":[]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./chunk-DGUM43GV.js";
|
|
2
2
|
|
|
3
|
-
// src/commands/import.ts
|
|
3
|
+
// src/commands/import-script.ts
|
|
4
4
|
import { promises as fs } from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { importScript, loadProject, parseScript } from "@ddt-tools/core";
|
|
8
|
-
function
|
|
9
|
-
const cmd = new Command("import");
|
|
8
|
+
function importScriptCommand() {
|
|
9
|
+
const cmd = new Command("import-script");
|
|
10
10
|
cmd.description(
|
|
11
11
|
"Parse a SQL script and write each DDL statement into the .ddtproj tree under its canonical folder."
|
|
12
12
|
).requiredOption("--script <path>", "Path to the SQL script to import.").requiredOption("-p, --project <path>", "Path to the .ddtproj file.").option("--dry-run", "Report what would be written without touching disk.", false).option("--force", "Overwrite existing files (default refuses on conflict).", false).option(
|
|
@@ -74,6 +74,6 @@ function printParseReport(scriptPath, parsed) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
export {
|
|
77
|
-
|
|
77
|
+
importScriptCommand
|
|
78
78
|
};
|
|
79
|
-
//# sourceMappingURL=import-
|
|
79
|
+
//# sourceMappingURL=import-script-R5RXPDH6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/import-script.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { importScript, loadProject, parseScript, type ParsedStatement } from '@ddt-tools/core';\n\n/**\n * `ddt import-script` — parse a SQL script and apply each DDL statement to a\n * `.ddtproj` tree, writing each into the canonical folder for its object\n * type (`catalogs/<cat>/schemas/<sch>/<type-folder>/<name>.sql`).\n *\n * This is the script→tree splitter. For converting another tool's artifacts\n * (Liquibase / Flyway / dbt / Terraform / …) into a fresh project, use\n * `ddt import` instead. Byte-aligned with `sdt import-script`.\n *\n * Default mode is safe:\n * - Parses the whole script first; refuses to write anything if any\n * statement has hard errors. The user reviews the report, fixes the\n * script, and re-runs.\n * - Refuses to overwrite existing files. Pass `--force` to clobber.\n * - `--dry-run` shows the plan without writing.\n */\nexport function importScriptCommand(): Command {\n const cmd = new Command('import-script');\n cmd\n .description(\n 'Parse a SQL script and write each DDL statement into the .ddtproj tree under its canonical folder.',\n )\n .requiredOption('--script <path>', 'Path to the SQL script to import.')\n .requiredOption('-p, --project <path>', 'Path to the .ddtproj file.')\n .option('--dry-run', 'Report what would be written without touching disk.', false)\n .option('--force', 'Overwrite existing files (default refuses on conflict).', false)\n .option(\n '--ignore-errors',\n 'Import classifiable statements even when others have errors. Default refuses on any error.',\n false,\n )\n .action(async (opts) => {\n const scriptPath = path.resolve(String(opts.script));\n const projectPath = path.resolve(String(opts.project));\n const sql = await fs.readFile(scriptPath, 'utf8');\n const parsed = parseScript(sql);\n\n // Up-front parse report — surfaces every error/warning verbatim.\n printParseReport(scriptPath, parsed);\n\n if (parsed.totalErrors > 0 && !opts.ignoreErrors) {\n console.error(\n `\\nRefusing to import: ${parsed.totalErrors} parse error(s) above. ` +\n `Fix the script and re-run, or pass --ignore-errors to import the clean statements.`,\n );\n process.exitCode = 2;\n return;\n }\n\n const loaded = await loadProject(projectPath);\n const result = await importScript(parsed, loaded, {\n dryRun: !!opts.dryRun,\n force: !!opts.force,\n });\n\n console.log('');\n const verb = opts.dryRun ? 'would write' : 'wrote';\n for (const item of result.imported) {\n const rel = path.relative(loaded.rootDir, item.targetPath);\n console.log(\n ` ${verb} ${item.statement.objectType} ${qualified(item.statement)} → ${rel}`,\n );\n }\n for (const item of result.skipped) {\n const head =\n item.statement.objectType && item.statement.fqn\n ? `${item.statement.objectType} ${qualified(item.statement)}`\n : `<unclassified statement at line ${item.statement.startLine}>`;\n console.log(` skip ${head} — ${item.reason}`);\n }\n console.log('');\n console.log(\n `Summary: ${result.imported.length} ${verb}, ${result.skipped.length} skipped, ` +\n `${parsed.totalErrors} errors, ${parsed.totalWarnings} warnings.`,\n );\n\n if (result.imported.length === 0 && result.skipped.length > 0) {\n process.exitCode = 1;\n }\n });\n return cmd;\n}\n\nfunction qualified(stmt: ParsedStatement): string {\n if (!stmt.fqn) return '<no fqn>';\n return [stmt.fqn.database, stmt.fqn.schema, stmt.fqn.name].filter(Boolean).join('.');\n}\n\nfunction printParseReport(scriptPath: string, parsed: ReturnType<typeof parseScript>): void {\n console.log(`Parsed ${parsed.statements.length} statement(s) from ${scriptPath}.`);\n if (parsed.totalErrors === 0 && parsed.totalWarnings === 0) {\n console.log('All statements classified cleanly.');\n return;\n }\n for (const stmt of parsed.statements) {\n if (stmt.errors.length === 0 && stmt.warnings.length === 0) continue;\n const head =\n stmt.objectType && stmt.fqn ? `${stmt.objectType} ${qualified(stmt)}` : '<unclassified>';\n console.log(`\\n[line ${stmt.startLine}–${stmt.endLine}] ${head}`);\n for (const e of stmt.errors) console.log(` ERROR: ${e}`);\n for (const w of stmt.warnings) console.log(` warning: ${w}`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
features
|
|
18
18
|
} from "@ddt-tools/core";
|
|
19
19
|
var SERVER_NAME = "ddt";
|
|
20
|
-
var SERVER_VERSION = "0.2.
|
|
20
|
+
var SERVER_VERSION = "0.2.5";
|
|
21
21
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
22
22
|
var TOOLS = [
|
|
23
23
|
{
|
|
@@ -340,4 +340,4 @@ function send(msg) {
|
|
|
340
340
|
export {
|
|
341
341
|
mcpCommand
|
|
342
342
|
};
|
|
343
|
-
//# sourceMappingURL=mcp-
|
|
343
|
+
//# sourceMappingURL=mcp-6ZXOAF7S.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/mcp.ts"],"sourcesContent":["/**\n * `ddt mcp` — Model Context Protocol server (stdio transport).\n *\n * Exposes DDT's read-only operations as MCP tools so AI agents (Claude\n * Desktop, Cursor, Continue, Claude Code, MCP-aware IDE plugins) can\n * call them. Speaks JSON-RPC 2.0 over newline-delimited JSON on stdio.\n *\n * To wire into Claude Desktop / Cursor / etc., add to the MCP config:\n *\n * {\n * \"mcpServers\": {\n * \"ddt\": { \"command\": \"ddt\", \"args\": [\"mcp\"] }\n * }\n * }\n *\n * Tools exposed:\n * - ddt.validate — schema check on a .ddtproj\n * - ddt.compare — pac↔pac diff (offline)\n * - ddt.docs — generate HTML schema docs\n * - ddt.erd — generate Mermaid ER diagram (Markdown)\n * - ddt.safety_assess — classify a diff's safety findings\n * - ddt.narrate_diff — structured payload (diff + safety + prompt\n * scaffold) so the calling agent's LLM can\n * generate a plain-English narration.\n * - ddt.detect_pii — scan a project/pac for likely PII columns.\n * - ddt.suggest_safer — saferAlternatives entries for every\n * UNRECOVERABLE / DESTRUCTIVE finding.\n * - ddt.feature_search — search the DDT feature catalog by keyword.\n *\n * Live-network tools (extract, publish) are deliberately NOT exposed\n * — they require profile credentials and we'd rather an agent not act\n * on those without explicit user invocation.\n */\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n PacSource,\n renderDocsReport,\n renderErdMarkdown,\n loadProject,\n parseProjectModel,\n discoverObjectFiles,\n pac,\n pii,\n safety,\n features,\n type DatabricksObject,\n} from '@ddt-tools/core';\n\ninterface JsonRpcRequest {\n jsonrpc: '2.0';\n id?: number | string | null;\n method: string;\n params?: unknown;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: '2.0';\n id: number | string | null;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\nconst SERVER_NAME = 'ddt';\nconst SERVER_VERSION = '0.2.4';\nconst PROTOCOL_VERSION = '2024-11-05';\n\ninterface ToolDef {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n handler: (args: Record<string, unknown>) => Promise<string>;\n}\n\nconst TOOLS: ToolDef[] = [\n {\n name: 'ddt.validate',\n description:\n 'Validate a .ddtproj file: parse JSON, check schema with zod, list object files. Returns a JSON summary of project structure.',\n inputSchema: {\n type: 'object',\n properties: {\n projectPath: { type: 'string', description: 'Absolute path to a .ddtproj file.' },\n },\n required: ['projectPath'],\n },\n handler: async (args) => {\n const projectPath = path.resolve(String(args.projectPath));\n const loaded = await loadProject(projectPath);\n const files = await discoverObjectFiles(loaded);\n return JSON.stringify(\n {\n ok: true,\n name: loaded.project.name,\n version: loaded.project.version,\n scope: loaded.project.scope,\n objectFileCount: files.length,\n profiles: Object.keys(loaded.project.deploymentProfiles ?? {}),\n },\n null,\n 2,\n );\n },\n },\n {\n name: 'ddt.compare',\n description:\n 'Compare two .ddtpac files (offline). Returns the diff JSON: added / removed / modified objects, with per-object field changes.',\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Absolute path to the source .ddtpac.' },\n targetPac: { type: 'string', description: 'Absolute path to the target .ddtpac.' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const result = await engine.compare(source, target);\n return JSON.stringify(result, null, 2);\n },\n },\n {\n name: 'ddt.docs',\n description:\n 'Generate self-contained HTML schema documentation from a .ddtproj or .ddtpac. Returns the HTML body as a string.',\n inputSchema: {\n type: 'object',\n properties: {\n source: { type: 'string', description: 'Absolute path to a .ddtproj or .ddtpac.' },\n title: { type: 'string', description: 'Optional doc title.' },\n },\n required: ['source'],\n },\n handler: async (args) => {\n const model = await loadModel(String(args.source));\n return renderDocsReport(model, {\n title: typeof args.title === 'string' ? args.title : 'Schema docs',\n });\n },\n },\n {\n name: 'ddt.erd',\n description: 'Generate a Mermaid ER diagram (Markdown) from a .ddtproj or .ddtpac.',\n inputSchema: {\n type: 'object',\n properties: {\n source: { type: 'string', description: 'Absolute path to a .ddtproj or .ddtpac.' },\n title: { type: 'string', description: 'Optional diagram title.' },\n },\n required: ['source'],\n },\n handler: async (args) => {\n const model = await loadModel(String(args.source));\n return renderErdMarkdown(\n model,\n typeof args.title === 'string' ? args.title : 'Schema ER diagram',\n );\n },\n },\n {\n name: 'ddt.safety_assess',\n description:\n 'Given two .ddtpac files, run the safety classifier on the diff. Returns UNRECOVERABLE / DESTRUCTIVE / EXPENSIVE / WARNING findings with the gate option each would need.',\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Source .ddtpac (desired state).' },\n targetPac: { type: 'string', description: 'Target .ddtpac (current state).' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const diff = await engine.compare(source, target);\n const assessment = safety.assess(diff);\n return JSON.stringify(assessment, null, 2);\n },\n },\n {\n name: 'ddt.narrate_diff',\n description:\n \"Build a structured payload (diff summary + per-object changes + safety findings + a system-prompt scaffold) the calling agent's LLM can use to generate a plain-English narration of a schema diff. Returns JSON the agent should compose into its own completion.\",\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Source .ddtpac (desired state).' },\n targetPac: { type: 'string', description: 'Target .ddtpac (current state).' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const diff = await engine.compare(source, target);\n const assessment = safety.assess(diff);\n const payload = {\n summary: diff.summary,\n objects: diff.objects.map((o) => ({\n kind: o.kind,\n objectType: o.identity.objectType,\n fqn: o.identity.fqn,\n })),\n safety: assessment,\n promptScaffold: {\n system:\n 'You are a senior Databricks UC engineer reviewing a schema migration. ' +\n 'Narrate the change in 2-3 short paragraphs: (1) the intent, ' +\n '(2) the safest order of operations, (3) the risks to watch ' +\n 'for. Be concrete; reference the FQNs.',\n userTemplate: 'Here is the diff JSON and the safety assessment. Generate the narration.',\n },\n };\n return JSON.stringify(payload, null, 2);\n },\n },\n {\n name: 'ddt.detect_pii',\n description:\n 'Scan a project or pac for columns that look like PII (email, phone, ssn, dob, credit-card, national-id, health, ip-address, name, address, generic). Returns each candidate with its category, confidence tier, and the reason the heuristic flagged it.',\n inputSchema: {\n type: 'object',\n properties: {\n source: { type: 'string', description: 'Absolute path to a .ddtproj or .ddtpac.' },\n },\n required: ['source'],\n },\n handler: async (args) => {\n const model = await loadModel(String(args.source));\n const candidates = pii.detectPiiCandidates(model);\n return JSON.stringify({ count: candidates.length, candidates }, null, 2);\n },\n },\n {\n name: 'ddt.suggest_safer',\n description:\n 'For every UNRECOVERABLE / DESTRUCTIVE finding the safety classifier produced on a diff, return the catalog\\'s saferAlternatives entries (e.g. \"instead of DROP TABLE, RENAME with a date suffix and let the retention policy expire it\"). The agent can paste these into a PR comment or CodeLens suggestion.',\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Source .ddtpac (desired).' },\n targetPac: { type: 'string', description: 'Target .ddtpac (current).' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const diff = await engine.compare(source, target);\n const assessment = safety.assess(diff);\n const dangerous = [...assessment.unrecoverable, ...assessment.destructive];\n const suggestions = dangerous.map((f) => {\n const explanation = safety.explainFinding(f.code);\n return {\n finding: { code: f.code, category: f.category, fqn: f.fqn, reason: f.reason },\n saferAlternatives: explanation?.saferAlternatives ?? [],\n requiredGates: explanation?.requiredGates ?? [],\n };\n });\n return JSON.stringify({ count: suggestions.length, suggestions }, null, 2);\n },\n },\n {\n name: 'ddt.feature_search',\n description:\n 'Search the DDT feature catalog by keyword. Returns features whose id, name, summary, or synonyms match the query. Useful for discovering which tier unlocks a capability or finding the right command for a task.',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description:\n 'Search term — feature name, keyword, or capability (e.g. \"drift\", \"slice\", \"AI\").',\n },\n },\n required: ['query'],\n },\n handler: async (args) => {\n const q = String(args.query ?? '')\n .toLowerCase()\n .trim();\n const matches = features.DDT_FEATURE_CATALOG.filter(\n (f) =>\n f.id.includes(q) ||\n f.name.toLowerCase().includes(q) ||\n f.summary.toLowerCase().includes(q) ||\n f.synonyms?.some((s) => s.toLowerCase().includes(q)) ||\n f.whenToUse?.toLowerCase().includes(q),\n );\n return JSON.stringify(\n {\n count: matches.length,\n features: matches.map((f) => ({\n id: f.id,\n name: f.name,\n tier: f.tier,\n status: f.status,\n summary: f.summary,\n unlockHow: f.unlockHow,\n useCases: f.useCases,\n relatedFeatures: f.relatedFeatures,\n })),\n },\n null,\n 2,\n );\n },\n },\n];\n\nasync function loadModel(sourcePath: string): Promise<DatabricksObject[]> {\n const abs = path.resolve(sourcePath);\n if (abs.endsWith('.ddtpac')) {\n const pacContents = await pac.readPac(abs);\n return pacContents.model;\n }\n const loaded = await loadProject(abs);\n const parsed = await parseProjectModel(loaded);\n return parsed as DatabricksObject[];\n}\n\nexport function mcpCommand(): Command {\n const cmd = new Command('mcp');\n cmd\n .description(\n 'Start the DDT Model Context Protocol server on stdio (for Claude / Cursor / agents).',\n )\n .action(async () => {\n await runMcpStdio();\n });\n return cmd;\n}\n\nasync function runMcpStdio(): Promise<void> {\n await new Promise<void>((resolve) => {\n let buffer = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n buffer += chunk;\n let nl = buffer.indexOf('\\n');\n while (nl !== -1) {\n const line = buffer.slice(0, nl).trim();\n buffer = buffer.slice(nl + 1);\n if (line.length > 0) {\n void handleLine(line);\n }\n nl = buffer.indexOf('\\n');\n }\n });\n process.stdin.on('end', () => resolve());\n process.stdin.on('close', () => resolve());\n });\n}\n\nasync function handleLine(line: string): Promise<void> {\n let req: JsonRpcRequest;\n try {\n req = JSON.parse(line) as JsonRpcRequest;\n } catch (err) {\n send({\n jsonrpc: '2.0',\n id: null,\n error: { code: -32700, message: 'Parse error', data: String(err) },\n });\n return;\n }\n const id = req.id ?? null;\n try {\n const result = await dispatch(req);\n if (id !== null) {\n send({ jsonrpc: '2.0', id, result });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n send({ jsonrpc: '2.0', id, error: { code: -32603, message } });\n }\n}\n\nasync function dispatch(req: JsonRpcRequest): Promise<unknown> {\n switch (req.method) {\n case 'initialize':\n return {\n protocolVersion: PROTOCOL_VERSION,\n serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },\n capabilities: { tools: {} },\n };\n case 'notifications/initialized':\n return undefined;\n case 'tools/list':\n return {\n tools: TOOLS.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n };\n case 'tools/call': {\n const params = (req.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };\n const tool = TOOLS.find((t) => t.name === params.name);\n if (!tool) throw new Error(`Unknown tool: ${String(params.name)}`);\n const output = await tool.handler(params.arguments ?? {});\n return { content: [{ type: 'text', text: output }] };\n }\n case 'ping':\n return {};\n default:\n throw new Error(`Method not found: ${req.method}`);\n }\n}\n\nfunction send(msg: JsonRpcResponse): void {\n process.stdout.write(`${JSON.stringify(msg)}\\n`);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/commands/mcp.ts"],"sourcesContent":["/**\n * `ddt mcp` — Model Context Protocol server (stdio transport).\n *\n * Exposes DDT's read-only operations as MCP tools so AI agents (Claude\n * Desktop, Cursor, Continue, Claude Code, MCP-aware IDE plugins) can\n * call them. Speaks JSON-RPC 2.0 over newline-delimited JSON on stdio.\n *\n * To wire into Claude Desktop / Cursor / etc., add to the MCP config:\n *\n * {\n * \"mcpServers\": {\n * \"ddt\": { \"command\": \"ddt\", \"args\": [\"mcp\"] }\n * }\n * }\n *\n * Tools exposed:\n * - ddt.validate — schema check on a .ddtproj\n * - ddt.compare — pac↔pac diff (offline)\n * - ddt.docs — generate HTML schema docs\n * - ddt.erd — generate Mermaid ER diagram (Markdown)\n * - ddt.safety_assess — classify a diff's safety findings\n * - ddt.narrate_diff — structured payload (diff + safety + prompt\n * scaffold) so the calling agent's LLM can\n * generate a plain-English narration.\n * - ddt.detect_pii — scan a project/pac for likely PII columns.\n * - ddt.suggest_safer — saferAlternatives entries for every\n * UNRECOVERABLE / DESTRUCTIVE finding.\n * - ddt.feature_search — search the DDT feature catalog by keyword.\n *\n * Live-network tools (extract, publish) are deliberately NOT exposed\n * — they require profile credentials and we'd rather an agent not act\n * on those without explicit user invocation.\n */\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n PacSource,\n renderDocsReport,\n renderErdMarkdown,\n loadProject,\n parseProjectModel,\n discoverObjectFiles,\n pac,\n pii,\n safety,\n features,\n type DatabricksObject,\n} from '@ddt-tools/core';\n\ninterface JsonRpcRequest {\n jsonrpc: '2.0';\n id?: number | string | null;\n method: string;\n params?: unknown;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: '2.0';\n id: number | string | null;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\nconst SERVER_NAME = 'ddt';\nconst SERVER_VERSION = '0.2.5';\nconst PROTOCOL_VERSION = '2024-11-05';\n\ninterface ToolDef {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n handler: (args: Record<string, unknown>) => Promise<string>;\n}\n\nconst TOOLS: ToolDef[] = [\n {\n name: 'ddt.validate',\n description:\n 'Validate a .ddtproj file: parse JSON, check schema with zod, list object files. Returns a JSON summary of project structure.',\n inputSchema: {\n type: 'object',\n properties: {\n projectPath: { type: 'string', description: 'Absolute path to a .ddtproj file.' },\n },\n required: ['projectPath'],\n },\n handler: async (args) => {\n const projectPath = path.resolve(String(args.projectPath));\n const loaded = await loadProject(projectPath);\n const files = await discoverObjectFiles(loaded);\n return JSON.stringify(\n {\n ok: true,\n name: loaded.project.name,\n version: loaded.project.version,\n scope: loaded.project.scope,\n objectFileCount: files.length,\n profiles: Object.keys(loaded.project.deploymentProfiles ?? {}),\n },\n null,\n 2,\n );\n },\n },\n {\n name: 'ddt.compare',\n description:\n 'Compare two .ddtpac files (offline). Returns the diff JSON: added / removed / modified objects, with per-object field changes.',\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Absolute path to the source .ddtpac.' },\n targetPac: { type: 'string', description: 'Absolute path to the target .ddtpac.' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const result = await engine.compare(source, target);\n return JSON.stringify(result, null, 2);\n },\n },\n {\n name: 'ddt.docs',\n description:\n 'Generate self-contained HTML schema documentation from a .ddtproj or .ddtpac. Returns the HTML body as a string.',\n inputSchema: {\n type: 'object',\n properties: {\n source: { type: 'string', description: 'Absolute path to a .ddtproj or .ddtpac.' },\n title: { type: 'string', description: 'Optional doc title.' },\n },\n required: ['source'],\n },\n handler: async (args) => {\n const model = await loadModel(String(args.source));\n return renderDocsReport(model, {\n title: typeof args.title === 'string' ? args.title : 'Schema docs',\n });\n },\n },\n {\n name: 'ddt.erd',\n description: 'Generate a Mermaid ER diagram (Markdown) from a .ddtproj or .ddtpac.',\n inputSchema: {\n type: 'object',\n properties: {\n source: { type: 'string', description: 'Absolute path to a .ddtproj or .ddtpac.' },\n title: { type: 'string', description: 'Optional diagram title.' },\n },\n required: ['source'],\n },\n handler: async (args) => {\n const model = await loadModel(String(args.source));\n return renderErdMarkdown(\n model,\n typeof args.title === 'string' ? args.title : 'Schema ER diagram',\n );\n },\n },\n {\n name: 'ddt.safety_assess',\n description:\n 'Given two .ddtpac files, run the safety classifier on the diff. Returns UNRECOVERABLE / DESTRUCTIVE / EXPENSIVE / WARNING findings with the gate option each would need.',\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Source .ddtpac (desired state).' },\n targetPac: { type: 'string', description: 'Target .ddtpac (current state).' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const diff = await engine.compare(source, target);\n const assessment = safety.assess(diff);\n return JSON.stringify(assessment, null, 2);\n },\n },\n {\n name: 'ddt.narrate_diff',\n description:\n \"Build a structured payload (diff summary + per-object changes + safety findings + a system-prompt scaffold) the calling agent's LLM can use to generate a plain-English narration of a schema diff. Returns JSON the agent should compose into its own completion.\",\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Source .ddtpac (desired state).' },\n targetPac: { type: 'string', description: 'Target .ddtpac (current state).' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const diff = await engine.compare(source, target);\n const assessment = safety.assess(diff);\n const payload = {\n summary: diff.summary,\n objects: diff.objects.map((o) => ({\n kind: o.kind,\n objectType: o.identity.objectType,\n fqn: o.identity.fqn,\n })),\n safety: assessment,\n promptScaffold: {\n system:\n 'You are a senior Databricks UC engineer reviewing a schema migration. ' +\n 'Narrate the change in 2-3 short paragraphs: (1) the intent, ' +\n '(2) the safest order of operations, (3) the risks to watch ' +\n 'for. Be concrete; reference the FQNs.',\n userTemplate: 'Here is the diff JSON and the safety assessment. Generate the narration.',\n },\n };\n return JSON.stringify(payload, null, 2);\n },\n },\n {\n name: 'ddt.detect_pii',\n description:\n 'Scan a project or pac for columns that look like PII (email, phone, ssn, dob, credit-card, national-id, health, ip-address, name, address, generic). Returns each candidate with its category, confidence tier, and the reason the heuristic flagged it.',\n inputSchema: {\n type: 'object',\n properties: {\n source: { type: 'string', description: 'Absolute path to a .ddtproj or .ddtpac.' },\n },\n required: ['source'],\n },\n handler: async (args) => {\n const model = await loadModel(String(args.source));\n const candidates = pii.detectPiiCandidates(model);\n return JSON.stringify({ count: candidates.length, candidates }, null, 2);\n },\n },\n {\n name: 'ddt.suggest_safer',\n description:\n 'For every UNRECOVERABLE / DESTRUCTIVE finding the safety classifier produced on a diff, return the catalog\\'s saferAlternatives entries (e.g. \"instead of DROP TABLE, RENAME with a date suffix and let the retention policy expire it\"). The agent can paste these into a PR comment or CodeLens suggestion.',\n inputSchema: {\n type: 'object',\n properties: {\n sourcePac: { type: 'string', description: 'Source .ddtpac (desired).' },\n targetPac: { type: 'string', description: 'Target .ddtpac (current).' },\n },\n required: ['sourcePac', 'targetPac'],\n },\n handler: async (args) => {\n const source = new PacSource(path.resolve(String(args.sourcePac)));\n const target = new PacSource(path.resolve(String(args.targetPac)));\n const engine = new CompareEngine();\n const diff = await engine.compare(source, target);\n const assessment = safety.assess(diff);\n const dangerous = [...assessment.unrecoverable, ...assessment.destructive];\n const suggestions = dangerous.map((f) => {\n const explanation = safety.explainFinding(f.code);\n return {\n finding: { code: f.code, category: f.category, fqn: f.fqn, reason: f.reason },\n saferAlternatives: explanation?.saferAlternatives ?? [],\n requiredGates: explanation?.requiredGates ?? [],\n };\n });\n return JSON.stringify({ count: suggestions.length, suggestions }, null, 2);\n },\n },\n {\n name: 'ddt.feature_search',\n description:\n 'Search the DDT feature catalog by keyword. Returns features whose id, name, summary, or synonyms match the query. Useful for discovering which tier unlocks a capability or finding the right command for a task.',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description:\n 'Search term — feature name, keyword, or capability (e.g. \"drift\", \"slice\", \"AI\").',\n },\n },\n required: ['query'],\n },\n handler: async (args) => {\n const q = String(args.query ?? '')\n .toLowerCase()\n .trim();\n const matches = features.DDT_FEATURE_CATALOG.filter(\n (f) =>\n f.id.includes(q) ||\n f.name.toLowerCase().includes(q) ||\n f.summary.toLowerCase().includes(q) ||\n f.synonyms?.some((s) => s.toLowerCase().includes(q)) ||\n f.whenToUse?.toLowerCase().includes(q),\n );\n return JSON.stringify(\n {\n count: matches.length,\n features: matches.map((f) => ({\n id: f.id,\n name: f.name,\n tier: f.tier,\n status: f.status,\n summary: f.summary,\n unlockHow: f.unlockHow,\n useCases: f.useCases,\n relatedFeatures: f.relatedFeatures,\n })),\n },\n null,\n 2,\n );\n },\n },\n];\n\nasync function loadModel(sourcePath: string): Promise<DatabricksObject[]> {\n const abs = path.resolve(sourcePath);\n if (abs.endsWith('.ddtpac')) {\n const pacContents = await pac.readPac(abs);\n return pacContents.model;\n }\n const loaded = await loadProject(abs);\n const parsed = await parseProjectModel(loaded);\n return parsed as DatabricksObject[];\n}\n\nexport function mcpCommand(): Command {\n const cmd = new Command('mcp');\n cmd\n .description(\n 'Start the DDT Model Context Protocol server on stdio (for Claude / Cursor / agents).',\n )\n .action(async () => {\n await runMcpStdio();\n });\n return cmd;\n}\n\nasync function runMcpStdio(): Promise<void> {\n await new Promise<void>((resolve) => {\n let buffer = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n buffer += chunk;\n let nl = buffer.indexOf('\\n');\n while (nl !== -1) {\n const line = buffer.slice(0, nl).trim();\n buffer = buffer.slice(nl + 1);\n if (line.length > 0) {\n void handleLine(line);\n }\n nl = buffer.indexOf('\\n');\n }\n });\n process.stdin.on('end', () => resolve());\n process.stdin.on('close', () => resolve());\n });\n}\n\nasync function handleLine(line: string): Promise<void> {\n let req: JsonRpcRequest;\n try {\n req = JSON.parse(line) as JsonRpcRequest;\n } catch (err) {\n send({\n jsonrpc: '2.0',\n id: null,\n error: { code: -32700, message: 'Parse error', data: String(err) },\n });\n return;\n }\n const id = req.id ?? null;\n try {\n const result = await dispatch(req);\n if (id !== null) {\n send({ jsonrpc: '2.0', id, result });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n send({ jsonrpc: '2.0', id, error: { code: -32603, message } });\n }\n}\n\nasync function dispatch(req: JsonRpcRequest): Promise<unknown> {\n switch (req.method) {\n case 'initialize':\n return {\n protocolVersion: PROTOCOL_VERSION,\n serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },\n capabilities: { tools: {} },\n };\n case 'notifications/initialized':\n return undefined;\n case 'tools/list':\n return {\n tools: TOOLS.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n };\n case 'tools/call': {\n const params = (req.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };\n const tool = TOOLS.find((t) => t.name === params.name);\n if (!tool) throw new Error(`Unknown tool: ${String(params.name)}`);\n const output = await tool.handler(params.arguments ?? {});\n return { content: [{ type: 'text', text: output }] };\n }\n case 'ping':\n return {};\n default:\n throw new Error(`Method not found: ${req.method}`);\n }\n}\n\nfunction send(msg: JsonRpcResponse): void {\n process.stdout.write(`${JSON.stringify(msg)}\\n`);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -15,6 +15,7 @@ import path from "path";
|
|
|
15
15
|
import {
|
|
16
16
|
CompareEngine,
|
|
17
17
|
DatabricksExecutor,
|
|
18
|
+
LiveSource,
|
|
18
19
|
PacSource,
|
|
19
20
|
ProjectSource,
|
|
20
21
|
ScriptGenerator,
|
|
@@ -45,12 +46,14 @@ import {
|
|
|
45
46
|
} from "@ddt-tools/core";
|
|
46
47
|
function publishCommand() {
|
|
47
48
|
const cmd = new Command("publish");
|
|
48
|
-
cmd.description(
|
|
49
|
+
cmd.description(
|
|
50
|
+
"Generate (and optionally apply) a migration script from source \u2192 target. Shared form with `sdt publish`: `--source <desired> --connection <live-target>` compares the desired state against the live workspace. Or pass `--target <offline>` to diff against a `.ddtproj` / `.ddtpac` instead."
|
|
51
|
+
).option(
|
|
49
52
|
"--source <path>",
|
|
50
53
|
"Source: .ddtproj or .ddtpac (the desired state). Required for a normal publish; omit only with --restore-from-snapshot."
|
|
51
54
|
).option(
|
|
52
55
|
"--target <path>",
|
|
53
|
-
"Target: .ddtproj or .ddtpac (the
|
|
56
|
+
"Target / current state: .ddtproj or .ddtpac (offline). Omit to compare the source against the live workspace at --connection (the `sdt publish` model). One of --target or --connection is required for a normal publish."
|
|
54
57
|
).option(
|
|
55
58
|
"--restore-from-snapshot <batchId>",
|
|
56
59
|
"Recovery mode: skip the compare step and emit DROP/CREATE \u2026 SHALLOW CLONE against the snapshot batch <batchId> from the registry. Dry-run by default; pass --apply --yes to execute. This is the command printed by TRUST.4's post-deploy-smoke + TRUST.8's restore-hint when a deploy fails \u2014 they tell the operator to run `ddt publish --restore-from-snapshot <id>`."
|
|
@@ -65,7 +68,10 @@ function publishCommand() {
|
|
|
65
68
|
"--apply",
|
|
66
69
|
"Apply the migration against --connection. Requires --connection. Default false.",
|
|
67
70
|
false
|
|
68
|
-
).option(
|
|
71
|
+
).option(
|
|
72
|
+
"--connection <name>",
|
|
73
|
+
"Connection profile. Doubles as the live compare target when --target is omitted, and as the apply executor (required with --apply)."
|
|
74
|
+
).option(
|
|
69
75
|
"--yes",
|
|
70
76
|
"Skip interactive confirmation when applying. Required with --apply (no TTY today).",
|
|
71
77
|
false
|
|
@@ -152,17 +158,31 @@ function publishCommand() {
|
|
|
152
158
|
await runRestoreFromSnapshot(opts);
|
|
153
159
|
return;
|
|
154
160
|
}
|
|
155
|
-
if (!opts.source
|
|
161
|
+
if (!opts.source) {
|
|
162
|
+
throw new Error("--source <path> is required (unless --restore-from-snapshot is given).");
|
|
163
|
+
}
|
|
164
|
+
if (!opts.target && !opts.connection) {
|
|
156
165
|
throw new Error(
|
|
157
|
-
"
|
|
166
|
+
"Provide a current state to diff the source against: --target <.ddtproj|.ddtpac> (offline) or --connection <profile> (the live workspace, mirroring `sdt publish`). With neither there is nothing to compare."
|
|
158
167
|
);
|
|
159
168
|
}
|
|
160
169
|
const nameMapping = await buildMappingFromOptions(opts);
|
|
161
170
|
const sourcePath = String(opts.source);
|
|
162
|
-
const targetPath = String(opts.target);
|
|
163
171
|
const engine = new CompareEngine();
|
|
164
172
|
const src = sourcePath.endsWith(".ddtpac") ? new PacSource(sourcePath, "source") : new ProjectSource(sourcePath, "source");
|
|
165
|
-
|
|
173
|
+
let tgt;
|
|
174
|
+
if (opts.target) {
|
|
175
|
+
const targetPath = String(opts.target);
|
|
176
|
+
tgt = targetPath.endsWith(".ddtpac") ? new PacSource(targetPath, "target") : new ProjectSource(targetPath, "target");
|
|
177
|
+
} else {
|
|
178
|
+
const scope = await resolveSourceScope(sourcePath);
|
|
179
|
+
const liveProfile = await getProfile(String(opts.connection));
|
|
180
|
+
tgt = new LiveSource(
|
|
181
|
+
createConnection(liveProfile),
|
|
182
|
+
scope,
|
|
183
|
+
`${String(opts.connection)} (live)`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
166
186
|
let loadedSrcPac;
|
|
167
187
|
if (sourcePath.endsWith(".ddtpac")) {
|
|
168
188
|
loadedSrcPac = await pacNs.readPac(sourcePath);
|
|
@@ -587,6 +607,13 @@ function printPlanReport(diffSummary, scriptSummary, assessment, steps) {
|
|
|
587
607
|
console.log(`${steps.length} step(s) planned (default-safe selection)`);
|
|
588
608
|
console.log(`${reversible} step(s) reversible \xB7 ${steps.length - reversible} irreversible`);
|
|
589
609
|
}
|
|
610
|
+
async function resolveSourceScope(sourcePath) {
|
|
611
|
+
const scope = sourcePath.endsWith(".ddtpac") ? (await pacNs.readPac(sourcePath)).manifest.scope : (await loadProject(sourcePath)).project.scope;
|
|
612
|
+
return {
|
|
613
|
+
...scope.catalog ? { catalog: scope.catalog } : {},
|
|
614
|
+
...scope.schema ? { schema: scope.schema } : {}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
590
617
|
async function postWebhook(url, payload) {
|
|
591
618
|
const isSlackOrTeams = /hooks\.slack\.com|webhook\.office\.com/.test(url);
|
|
592
619
|
let body;
|
|
@@ -736,4 +763,4 @@ export {
|
|
|
736
763
|
publishCommand,
|
|
737
764
|
resolveQueryTag
|
|
738
765
|
};
|
|
739
|
-
//# sourceMappingURL=publish-
|
|
766
|
+
//# sourceMappingURL=publish-HLP3XHM5.js.map
|