@sdt-tools/cli 0.2.0 → 0.2.5
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/advise-tests-6DRSZMBL.js +87 -0
- package/dist/advise-tests-6DRSZMBL.js.map +1 -0
- package/dist/ai-G4MJWHTM.js +89 -0
- package/dist/ai-G4MJWHTM.js.map +1 -0
- package/dist/anonymize-QR6JGXA7.js +123 -0
- package/dist/anonymize-QR6JGXA7.js.map +1 -0
- package/dist/approval-YVHYTV53.js +73 -0
- package/dist/approval-YVHYTV53.js.map +1 -0
- package/dist/approval-chain-54KKJZS3.js +120 -0
- package/dist/approval-chain-54KKJZS3.js.map +1 -0
- package/dist/audit-log-QZFH7LUX.js +159 -0
- package/dist/audit-log-QZFH7LUX.js.map +1 -0
- package/dist/backlog-V2YUIQDL.js +76 -0
- package/dist/backlog-V2YUIQDL.js.map +1 -0
- package/dist/bisect-GEVYAVL5.js +111 -0
- package/dist/bisect-GEVYAVL5.js.map +1 -0
- package/dist/bookmarks-57LKS7P6.js +107 -0
- package/dist/bookmarks-57LKS7P6.js.map +1 -0
- package/dist/branch-W2MGMPSH.js +88 -0
- package/dist/branch-W2MGMPSH.js.map +1 -0
- package/dist/build-VNIQFKSP.js +23 -0
- package/dist/build-VNIQFKSP.js.map +1 -0
- package/dist/catalog-JLB5VCEV.js +137 -0
- package/dist/catalog-JLB5VCEV.js.map +1 -0
- package/dist/changelog-M7XGDYSY.js +220 -0
- package/dist/changelog-M7XGDYSY.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-EWXM4KJN.js +25 -0
- package/dist/chunk-EWXM4KJN.js.map +1 -0
- package/dist/chunk-JP2EZLR5.js +50 -0
- package/dist/chunk-JP2EZLR5.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-ZWY4ZRHL.js +44 -0
- package/dist/chunk-ZWY4ZRHL.js.map +1 -0
- package/dist/cli.js +506 -19014
- package/dist/cli.js.map +1 -1
- package/dist/compare-5O6UTWPJ.js +405 -0
- package/dist/compare-5O6UTWPJ.js.map +1 -0
- package/dist/compare-profiles-7ZSNIW7B.js +218 -0
- package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
- package/dist/completion-I5U5VVAX.js +82 -0
- package/dist/completion-I5U5VVAX.js.map +1 -0
- package/dist/connection-SYTH4V53.js +110 -0
- package/dist/connection-SYTH4V53.js.map +1 -0
- package/dist/cost-estimate-TJDDH6TO.js +328 -0
- package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
- package/dist/data-compare-UK2UXAS3.js +134 -0
- package/dist/data-compare-UK2UXAS3.js.map +1 -0
- package/dist/data-fit-Q45ENBRL.js +125 -0
- package/dist/data-fit-Q45ENBRL.js.map +1 -0
- package/dist/deploy-status-UUHKVDTI.js +58 -0
- package/dist/deploy-status-UUHKVDTI.js.map +1 -0
- package/dist/design-PO6UPBL7.js +138 -0
- package/dist/design-PO6UPBL7.js.map +1 -0
- package/dist/diagnose-6IFMELFR.js +145 -0
- package/dist/diagnose-6IFMELFR.js.map +1 -0
- package/dist/discover-A7OSZAHK.js +78 -0
- package/dist/discover-A7OSZAHK.js.map +1 -0
- package/dist/docs-CVRKGUSW.js +177 -0
- package/dist/docs-CVRKGUSW.js.map +1 -0
- package/dist/drift-XDA3BDYN.js +226 -0
- package/dist/drift-XDA3BDYN.js.map +1 -0
- package/dist/drift-gate-V7QSIOGZ.js +94 -0
- package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
- package/dist/error-lookup-7ZWCZJ44.js +56 -0
- package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
- package/dist/errorReporting-ZRNJ3VW7.js +109 -0
- package/dist/errorReporting-ZRNJ3VW7.js.map +1 -0
- package/dist/exec-PKBHLI7T.js +121 -0
- package/dist/exec-PKBHLI7T.js.map +1 -0
- package/dist/explain-LWKJOTL7.js +192 -0
- package/dist/explain-LWKJOTL7.js.map +1 -0
- package/dist/explorer-QOVM6VBD.js +61 -0
- package/dist/explorer-QOVM6VBD.js.map +1 -0
- package/dist/export-IYYBZ5HE.js +42 -0
- package/dist/export-IYYBZ5HE.js.map +1 -0
- package/dist/extract-VMMVRQVT.js +102 -0
- package/dist/extract-VMMVRQVT.js.map +1 -0
- package/dist/features-LE6BDZ2S.js +59 -0
- package/dist/features-LE6BDZ2S.js.map +1 -0
- package/dist/feedback-M7DM2EQC.js +161 -0
- package/dist/feedback-M7DM2EQC.js.map +1 -0
- package/dist/find-EME2JG2I.js +176 -0
- package/dist/find-EME2JG2I.js.map +1 -0
- package/dist/format-TRLWLMGS.js +141 -0
- package/dist/format-TRLWLMGS.js.map +1 -0
- package/dist/generate-6NAZGZDV.js +152 -0
- package/dist/generate-6NAZGZDV.js.map +1 -0
- package/dist/graph-QNQDAUO7.js +161 -0
- package/dist/graph-QNQDAUO7.js.map +1 -0
- package/dist/history-RONA7ZTI.js +199 -0
- package/dist/history-RONA7ZTI.js.map +1 -0
- package/dist/hosts-YBXY2ZG5.js +49 -0
- package/dist/hosts-YBXY2ZG5.js.map +1 -0
- package/dist/impact-T2JSANHS.js +59 -0
- package/dist/impact-T2JSANHS.js.map +1 -0
- package/dist/import-AELYLY6A.js +32 -0
- package/dist/import-AELYLY6A.js.map +1 -0
- package/dist/index.cjs +36 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +60 -25
- package/dist/index.js.map +1 -1
- package/dist/init-SWRRJMGI.js +57 -0
- package/dist/init-SWRRJMGI.js.map +1 -0
- package/dist/install-hooks-6SIAGTAF.js +109 -0
- package/dist/install-hooks-6SIAGTAF.js.map +1 -0
- package/dist/license-OAF22PLZ.js +46 -0
- package/dist/license-OAF22PLZ.js.map +1 -0
- package/dist/lineage-EW66XJ6O.js +552 -0
- package/dist/lineage-EW66XJ6O.js.map +1 -0
- package/dist/lint-FQ2OTYTQ.js +143 -0
- package/dist/lint-FQ2OTYTQ.js.map +1 -0
- package/dist/mcp-3QI4TH4N.js +344 -0
- package/dist/mcp-3QI4TH4N.js.map +1 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
- package/dist/migrate-platform-NTRTOGNR.js +91 -0
- package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
- package/dist/optimize-CJYWMAWA.js +105 -0
- package/dist/optimize-CJYWMAWA.js.map +1 -0
- package/dist/perf-LL2CPCJF.js +205 -0
- package/dist/perf-LL2CPCJF.js.map +1 -0
- package/dist/pii-FBDRDQ2E.js +136 -0
- package/dist/pii-FBDRDQ2E.js.map +1 -0
- package/dist/pilot-CCQERKPH.js +29 -0
- package/dist/pilot-CCQERKPH.js.map +1 -0
- package/dist/pr-comment-S5FF4QRX.js +79 -0
- package/dist/pr-comment-S5FF4QRX.js.map +1 -0
- package/dist/preview-5U4YVCRM.js +47 -0
- package/dist/preview-5U4YVCRM.js.map +1 -0
- package/dist/profile-7VC57KD2.js +101 -0
- package/dist/profile-7VC57KD2.js.map +1 -0
- package/dist/promote-AASEFTIA.js +408 -0
- package/dist/promote-AASEFTIA.js.map +1 -0
- package/dist/publish-Y2J56K4Y.js +715 -0
- package/dist/publish-Y2J56K4Y.js.map +1 -0
- package/dist/purge-QMXZKCMD.js +57 -0
- package/dist/purge-QMXZKCMD.js.map +1 -0
- package/dist/query-log-6OM4GI7W.js +112 -0
- package/dist/query-log-6OM4GI7W.js.map +1 -0
- package/dist/refactor-LTZQLJ35.js +5799 -0
- package/dist/refactor-LTZQLJ35.js.map +1 -0
- package/dist/refresh-4TY2AGOU.js +38 -0
- package/dist/refresh-4TY2AGOU.js.map +1 -0
- package/dist/replay-OOC25FZN.js +117 -0
- package/dist/replay-OOC25FZN.js.map +1 -0
- package/dist/revert-ODMUVJW6.js +110 -0
- package/dist/revert-ODMUVJW6.js.map +1 -0
- package/dist/review-XXPWOBFP.js +158 -0
- package/dist/review-XXPWOBFP.js.map +1 -0
- package/dist/rollback-suggest-6G2HEKFR.js +79 -0
- package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
- package/dist/safer-alternative-QFVNLG3L.js +89 -0
- package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
- package/dist/safety-7QWRSUEZ.js +168 -0
- package/dist/safety-7QWRSUEZ.js.map +1 -0
- package/dist/savings-RHIXP6IT.js +95 -0
- package/dist/savings-RHIXP6IT.js.map +1 -0
- package/dist/scan-secrets-5YCQ4UCU.js +54 -0
- package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
- package/dist/schema-CIZXCQD2.js +429 -0
- package/dist/schema-CIZXCQD2.js.map +1 -0
- package/dist/script-K7CIN2P6.js +153 -0
- package/dist/script-K7CIN2P6.js.map +1 -0
- package/dist/search-BUZ5NXZZ.js +151 -0
- package/dist/search-BUZ5NXZZ.js.map +1 -0
- package/dist/seed-76QAK276.js +96 -0
- package/dist/seed-76QAK276.js.map +1 -0
- package/dist/sketch-PTLKDIK3.js +88 -0
- package/dist/sketch-PTLKDIK3.js.map +1 -0
- package/dist/snapshot-XLPR2OZ5.js +177 -0
- package/dist/snapshot-XLPR2OZ5.js.map +1 -0
- package/dist/snippets-EK4DK5CN.js +74 -0
- package/dist/snippets-EK4DK5CN.js.map +1 -0
- package/dist/standards-7T2UY6DD.js +241 -0
- package/dist/standards-7T2UY6DD.js.map +1 -0
- package/dist/suggest-VGRYSAR6.js +39 -0
- package/dist/suggest-VGRYSAR6.js.map +1 -0
- package/dist/suggest-constraints-MY5WKUHA.js +160 -0
- package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
- package/dist/suite-TRNGZWQM.js +88 -0
- package/dist/suite-TRNGZWQM.js.map +1 -0
- package/dist/telemetry-3U2QLA2S.js +75 -0
- package/dist/telemetry-3U2QLA2S.js.map +1 -0
- package/dist/template-ZERIXVXF.js +403 -0
- package/dist/template-ZERIXVXF.js.map +1 -0
- package/dist/test-5M2ED3WT.js +169 -0
- package/dist/test-5M2ED3WT.js.map +1 -0
- package/dist/trial-U732FONV.js +31 -0
- package/dist/trial-U732FONV.js.map +1 -0
- package/dist/validate-T6D2WCOK.js +106 -0
- package/dist/validate-T6D2WCOK.js.map +1 -0
- package/dist/verify-KXVASEEG.js +76 -0
- package/dist/verify-KXVASEEG.js.map +1 -0
- package/dist/watch-I6K4BNMA.js +80 -0
- package/dist/watch-I6K4BNMA.js.map +1 -0
- package/dist/xcompare-TPFLQO6W.js +87 -0
- package/dist/xcompare-TPFLQO6W.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19040
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-VM2H4LAO.js";
|
|
4
|
+
import "./chunk-DGUM43GV.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/suggest-constraints.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { ai, suggestConstraints as core, refactoring } from "@sdt-tools/core";
|
|
11
|
+
var CONFIDENCE_RANK = { low: 0, medium: 1, high: 2 };
|
|
12
|
+
function suggestConstraintsCommand() {
|
|
13
|
+
const cmd = new Command("suggest-constraints");
|
|
14
|
+
cmd.description(
|
|
15
|
+
"Suggest PK/UK/FK/CHECK candidates for a profiled table. Reads a TableProfile JSON (from `sdt profile`) and emits suggestions with confidence tiers + rationale."
|
|
16
|
+
).argument("<fqn>", "Fully-qualified table name (must match the profile.fqn).").requiredOption("--profile <path>", "JSON file containing the TableProfile to analyze.").option(
|
|
17
|
+
"--parents <path>",
|
|
18
|
+
"JSON file with parent PK directory: [{ fqn, columns }]. Enables FK candidates."
|
|
19
|
+
).option("--include-low", "Include low-confidence suggestions. Default: filtered out.", false).option(
|
|
20
|
+
"--default-enforced",
|
|
21
|
+
"Treat default platform enforcement as true (Hybrid Table semantics).",
|
|
22
|
+
false
|
|
23
|
+
).option(
|
|
24
|
+
"--ai",
|
|
25
|
+
"After heuristic scoring, re-rank by business plausibility via the configured AI provider. Requires `sdt ai` configured.",
|
|
26
|
+
false
|
|
27
|
+
).option(
|
|
28
|
+
"--ai-context <text>",
|
|
29
|
+
'Optional one-line context about the table (e.g. "customer events from the e-commerce app") to ground the AI re-ranker.'
|
|
30
|
+
).option(
|
|
31
|
+
"--apply-to <refactor-json-path>",
|
|
32
|
+
"Append included suggestions to a refactor.json file as ADD_CONSTRAINT ops. The file is created if missing; the existing operations array is preserved."
|
|
33
|
+
).option(
|
|
34
|
+
"--apply-min <tier>",
|
|
35
|
+
"Minimum confidence tier to apply (low | medium | high). Default: medium.",
|
|
36
|
+
"medium"
|
|
37
|
+
).option("--format <fmt>", "Output format: json | markdown. Default markdown.", "markdown").option("-o, --output <path>", "Write output to a file instead of stdout.").action(async (fqn, opts) => {
|
|
38
|
+
await runSuggestConstraints(fqn, opts);
|
|
39
|
+
});
|
|
40
|
+
return cmd;
|
|
41
|
+
}
|
|
42
|
+
async function runSuggestConstraints(fqn, opts) {
|
|
43
|
+
const profilePath = String(opts.profile);
|
|
44
|
+
const profileJson = await fs.readFile(path.resolve(profilePath), "utf8");
|
|
45
|
+
const tableProfile = JSON.parse(profileJson);
|
|
46
|
+
if (!tableProfile.fqn || !Array.isArray(tableProfile.columns)) {
|
|
47
|
+
throw new Error(`--profile file is not a TableProfile (missing fqn / columns).`);
|
|
48
|
+
}
|
|
49
|
+
if (tableProfile.fqn !== fqn) {
|
|
50
|
+
logger.warn(
|
|
51
|
+
`profile fqn "${tableProfile.fqn}" does not match argument "${fqn}". Using the argument.`
|
|
52
|
+
);
|
|
53
|
+
tableProfile.fqn = fqn;
|
|
54
|
+
}
|
|
55
|
+
let parentPks = [];
|
|
56
|
+
if (opts.parents) {
|
|
57
|
+
const parentsJson = await fs.readFile(path.resolve(String(opts.parents)), "utf8");
|
|
58
|
+
const parsed = JSON.parse(parentsJson);
|
|
59
|
+
if (!Array.isArray(parsed)) {
|
|
60
|
+
throw new Error(`--parents file must be a JSON array of { fqn, columns } entries.`);
|
|
61
|
+
}
|
|
62
|
+
parentPks = parsed;
|
|
63
|
+
}
|
|
64
|
+
let suggestions = core.suggestConstraints(tableProfile, {
|
|
65
|
+
parentPks,
|
|
66
|
+
includeLow: opts.includeLow === true,
|
|
67
|
+
defaultEnforced: opts.defaultEnforced === true
|
|
68
|
+
});
|
|
69
|
+
if (opts.ai === true && suggestions.length > 0) {
|
|
70
|
+
try {
|
|
71
|
+
suggestions = await core.rerankWithAi(suggestions, {
|
|
72
|
+
completeFn: async (prompt) => {
|
|
73
|
+
const reply = await ai.complete([{ role: "user", content: prompt }], {
|
|
74
|
+
feature: "suggest-constraints.rerank"
|
|
75
|
+
});
|
|
76
|
+
return reply.text;
|
|
77
|
+
},
|
|
78
|
+
context: typeof opts.aiContext === "string" ? opts.aiContext : void 0
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warn(
|
|
82
|
+
`--ai re-rank failed; keeping heuristic ordering: ${err instanceof Error ? err.message : String(err)}`
|
|
83
|
+
);
|
|
84
|
+
logger.warn("Run `sdt ai status` to verify your AI provider is configured.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (opts.applyTo) {
|
|
88
|
+
const applied = await applyToRefactorLog(
|
|
89
|
+
suggestions,
|
|
90
|
+
String(opts.applyTo),
|
|
91
|
+
fqn,
|
|
92
|
+
String(opts.applyMin ?? "medium").toLowerCase(),
|
|
93
|
+
opts.ai === true ? "ai-rerank" : "heuristic"
|
|
94
|
+
);
|
|
95
|
+
logger.info(
|
|
96
|
+
`--apply-to: appended ${applied} suggestion(s) as ADD_CONSTRAINT ops to ${opts.applyTo} (filtered by min confidence ${opts.applyMin ?? "medium"}).`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const format = String(opts.format ?? "markdown").toLowerCase();
|
|
100
|
+
const output = format === "json" ? JSON.stringify(suggestions, null, 2) : core.renderSuggestionsMarkdown(suggestions);
|
|
101
|
+
if (opts.output) {
|
|
102
|
+
await fs.writeFile(path.resolve(String(opts.output)), output, "utf8");
|
|
103
|
+
logger.info(`Wrote ${suggestions.length} suggestion(s) to ${opts.output}`);
|
|
104
|
+
} else {
|
|
105
|
+
process.stdout.write(output + (output.endsWith("\n") ? "" : "\n"));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function applyToRefactorLog(suggestions, refactorJsonPath, fqn, minConfidence, source) {
|
|
109
|
+
const fqnParts = fqn.split(".").filter(Boolean);
|
|
110
|
+
if (fqnParts.length < 1) {
|
|
111
|
+
throw new Error(`--apply-to: fqn "${fqn}" cannot be parsed`);
|
|
112
|
+
}
|
|
113
|
+
const objectRef = {
|
|
114
|
+
objectType: "TABLE",
|
|
115
|
+
name: fqnParts[fqnParts.length - 1],
|
|
116
|
+
...fqnParts.length >= 2 ? { schema: fqnParts[fqnParts.length - 2] } : {},
|
|
117
|
+
...fqnParts.length >= 3 ? { database: fqnParts[fqnParts.length - 3] } : {}
|
|
118
|
+
};
|
|
119
|
+
const absPath = path.resolve(refactorJsonPath);
|
|
120
|
+
let log = await refactoring.loadRefactorLog(absPath);
|
|
121
|
+
const minRank = CONFIDENCE_RANK[minConfidence] ?? CONFIDENCE_RANK.medium;
|
|
122
|
+
let appended = 0;
|
|
123
|
+
for (const s of suggestions) {
|
|
124
|
+
if ((CONFIDENCE_RANK[s.confidence] ?? 0) < minRank) continue;
|
|
125
|
+
const kind = mapKind(s.kind);
|
|
126
|
+
const details = {
|
|
127
|
+
constraintKind: kind,
|
|
128
|
+
columns: s.columns,
|
|
129
|
+
fromSuggester: true,
|
|
130
|
+
suggestionSource: source,
|
|
131
|
+
...s.references ? { referencedFqn: parseFqn(s.references.fqn), referencedColumns: s.references.columns } : {},
|
|
132
|
+
...s.predicate ? { expression: s.predicate } : {}
|
|
133
|
+
};
|
|
134
|
+
log = refactoring.appendOperation(log, {
|
|
135
|
+
type: "ADD_CONSTRAINT",
|
|
136
|
+
object: objectRef,
|
|
137
|
+
details
|
|
138
|
+
});
|
|
139
|
+
appended++;
|
|
140
|
+
}
|
|
141
|
+
await refactoring.saveRefactorLog(absPath, log);
|
|
142
|
+
return appended;
|
|
143
|
+
}
|
|
144
|
+
function mapKind(k) {
|
|
145
|
+
return k;
|
|
146
|
+
}
|
|
147
|
+
function parseFqn(s) {
|
|
148
|
+
const parts = s.split(".").filter(Boolean);
|
|
149
|
+
const name = parts[parts.length - 1];
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
...parts.length >= 2 ? { schema: parts[parts.length - 2] } : {},
|
|
153
|
+
...parts.length >= 3 ? { database: parts[parts.length - 3] } : {}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
runSuggestConstraints,
|
|
158
|
+
suggestConstraintsCommand
|
|
159
|
+
};
|
|
160
|
+
//# sourceMappingURL=suggest-constraints-MY5WKUHA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/suggest-constraints.ts"],"sourcesContent":["/**\n * `sdt suggest-constraints <fqn>` — item 5 of the constraint-enforcement\n * initiative. Reads a TableProfile JSON file (from `sdt profile`) plus\n * an optional parent-PKs JSON file, and emits heuristic PK/UK/FK/CHECK\n * candidates.\n *\n * v1 ships the heuristic path; `--ai` re-rank by business plausibility\n * and `--apply` (writes to refactor.json via applyChatOps) land in\n * follow-ups so this commit stays scope-bounded.\n *\n * Mirrors `ddt suggest-constraints`.\n */\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { Command } from 'commander';\nimport type { profile as profileNs } from '@sdt-tools/core';\nimport { ai, suggestConstraints as core, refactoring } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\n\ntype Confidence = 'low' | 'medium' | 'high';\nconst CONFIDENCE_RANK: Record<Confidence, number> = { low: 0, medium: 1, high: 2 };\n\nexport function suggestConstraintsCommand(): Command {\n const cmd = new Command('suggest-constraints');\n cmd\n .description(\n 'Suggest PK/UK/FK/CHECK candidates for a profiled table. Reads a TableProfile JSON ' +\n '(from `sdt profile`) and emits suggestions with confidence tiers + rationale.',\n )\n .argument('<fqn>', 'Fully-qualified table name (must match the profile.fqn).')\n .requiredOption('--profile <path>', 'JSON file containing the TableProfile to analyze.')\n .option(\n '--parents <path>',\n 'JSON file with parent PK directory: [{ fqn, columns }]. Enables FK candidates.',\n )\n .option('--include-low', 'Include low-confidence suggestions. Default: filtered out.', false)\n .option(\n '--default-enforced',\n 'Treat default platform enforcement as true (Hybrid Table semantics).',\n false,\n )\n .option(\n '--ai',\n 'After heuristic scoring, re-rank by business plausibility via the configured AI provider. Requires `sdt ai` configured.',\n false,\n )\n .option(\n '--ai-context <text>',\n 'Optional one-line context about the table (e.g. \"customer events from the e-commerce app\") to ground the AI re-ranker.',\n )\n .option(\n '--apply-to <refactor-json-path>',\n 'Append included suggestions to a refactor.json file as ADD_CONSTRAINT ops. The file is created if missing; the existing operations array is preserved.',\n )\n .option(\n '--apply-min <tier>',\n 'Minimum confidence tier to apply (low | medium | high). Default: medium.',\n 'medium',\n )\n .option('--format <fmt>', 'Output format: json | markdown. Default markdown.', 'markdown')\n .option('-o, --output <path>', 'Write output to a file instead of stdout.')\n .action(async (fqn: string, opts: Record<string, unknown>) => {\n await runSuggestConstraints(fqn, opts);\n });\n return cmd;\n}\n\nexport async function runSuggestConstraints(\n fqn: string,\n opts: Record<string, unknown>,\n): Promise<void> {\n const profilePath = String(opts.profile);\n const profileJson = await fs.readFile(path.resolve(profilePath), 'utf8');\n const tableProfile = JSON.parse(profileJson) as profileNs.TableProfile;\n if (!tableProfile.fqn || !Array.isArray(tableProfile.columns)) {\n throw new Error(`--profile file is not a TableProfile (missing fqn / columns).`);\n }\n if (tableProfile.fqn !== fqn) {\n logger.warn(\n `profile fqn \"${tableProfile.fqn}\" does not match argument \"${fqn}\". Using the argument.`,\n );\n tableProfile.fqn = fqn;\n }\n\n let parentPks: Array<{ fqn: string; columns: string[] }> = [];\n if (opts.parents) {\n const parentsJson = await fs.readFile(path.resolve(String(opts.parents)), 'utf8');\n const parsed = JSON.parse(parentsJson);\n if (!Array.isArray(parsed)) {\n throw new Error(`--parents file must be a JSON array of { fqn, columns } entries.`);\n }\n parentPks = parsed;\n }\n\n let suggestions: core.ConstraintSuggestion[] = core.suggestConstraints(tableProfile, {\n parentPks,\n includeLow: opts.includeLow === true,\n defaultEnforced: opts.defaultEnforced === true,\n });\n\n if (opts.ai === true && suggestions.length > 0) {\n try {\n suggestions = await core.rerankWithAi(suggestions, {\n completeFn: async (prompt: string) => {\n const reply = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'suggest-constraints.rerank',\n });\n return reply.text;\n },\n context: typeof opts.aiContext === 'string' ? opts.aiContext : undefined,\n });\n } catch (err) {\n logger.warn(\n `--ai re-rank failed; keeping heuristic ordering: ${err instanceof Error ? err.message : String(err)}`,\n );\n logger.warn('Run `sdt ai status` to verify your AI provider is configured.');\n }\n }\n\n if (opts.applyTo) {\n const applied = await applyToRefactorLog(\n suggestions,\n String(opts.applyTo),\n fqn,\n String(opts.applyMin ?? 'medium').toLowerCase() as Confidence,\n opts.ai === true ? 'ai-rerank' : 'heuristic',\n );\n logger.info(\n `--apply-to: appended ${applied} suggestion(s) as ADD_CONSTRAINT ops to ${opts.applyTo} (filtered by min confidence ${opts.applyMin ?? 'medium'}).`,\n );\n }\n\n const format = String(opts.format ?? 'markdown').toLowerCase();\n const output =\n format === 'json'\n ? JSON.stringify(suggestions, null, 2)\n : core.renderSuggestionsMarkdown(suggestions);\n\n if (opts.output) {\n await fs.writeFile(path.resolve(String(opts.output)), output, 'utf8');\n logger.info(`Wrote ${suggestions.length} suggestion(s) to ${opts.output}`);\n } else {\n process.stdout.write(output + (output.endsWith('\\n') ? '' : '\\n'));\n }\n}\n\nasync function applyToRefactorLog(\n suggestions: readonly core.ConstraintSuggestion[],\n refactorJsonPath: string,\n fqn: string,\n minConfidence: Confidence,\n source: 'heuristic' | 'ai-rerank',\n): Promise<number> {\n const fqnParts = fqn.split('.').filter(Boolean);\n if (fqnParts.length < 1) {\n throw new Error(`--apply-to: fqn \"${fqn}\" cannot be parsed`);\n }\n const objectRef = {\n objectType: 'TABLE' as const,\n name: fqnParts[fqnParts.length - 1]!,\n ...(fqnParts.length >= 2 ? { schema: fqnParts[fqnParts.length - 2]! } : {}),\n ...(fqnParts.length >= 3 ? { database: fqnParts[fqnParts.length - 3]! } : {}),\n };\n\n const absPath = path.resolve(refactorJsonPath);\n let log = await refactoring.loadRefactorLog(absPath);\n const minRank = CONFIDENCE_RANK[minConfidence] ?? CONFIDENCE_RANK.medium;\n let appended = 0;\n for (const s of suggestions) {\n if ((CONFIDENCE_RANK[s.confidence as Confidence] ?? 0) < minRank) continue;\n const kind = mapKind(s.kind);\n const details: refactoring.AddConstraintOp['details'] = {\n constraintKind: kind,\n columns: s.columns,\n fromSuggester: true,\n suggestionSource: source,\n ...(s.references\n ? { referencedFqn: parseFqn(s.references.fqn), referencedColumns: s.references.columns }\n : {}),\n ...(s.predicate ? { expression: s.predicate } : {}),\n };\n log = refactoring.appendOperation(log, {\n type: 'ADD_CONSTRAINT',\n object: objectRef,\n details,\n });\n appended++;\n }\n await refactoring.saveRefactorLog(absPath, log);\n return appended;\n}\n\nfunction mapKind(k: core.ConstraintKind): 'PRIMARY_KEY' | 'UNIQUE' | 'FOREIGN_KEY' | 'CHECK' {\n return k;\n}\n\nfunction parseFqn(s: string): { database?: string; schema?: string; name: string } {\n const parts = s.split('.').filter(Boolean);\n const name = parts[parts.length - 1]!;\n return {\n name,\n ...(parts.length >= 2 ? { schema: parts[parts.length - 2]! } : {}),\n ...(parts.length >= 3 ? { database: parts[parts.length - 3]! } : {}),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-VM2H4LAO.js";
|
|
4
|
+
import "./chunk-DGUM43GV.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/suite.ts
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import {
|
|
10
|
+
loadSuite,
|
|
11
|
+
newSuiteTemplate,
|
|
12
|
+
saveSuite,
|
|
13
|
+
validateSuite
|
|
14
|
+
} from "@sdt-tools/core/project";
|
|
15
|
+
import { license as licenseApi } from "@sdt-tools/core";
|
|
16
|
+
async function warnIfMissingLicense(feature) {
|
|
17
|
+
const lic = await licenseApi.loadLicense();
|
|
18
|
+
const r = licenseApi.checkProFeature(lic, feature);
|
|
19
|
+
if (r.warning) logger.warn(`[Pro] ${r.warning}`);
|
|
20
|
+
}
|
|
21
|
+
function suiteCommand() {
|
|
22
|
+
const cmd = new Command("suite").description(
|
|
23
|
+
"Manage a .sdtsuite \u2014 a collection of .sdtproj projects deploying together."
|
|
24
|
+
);
|
|
25
|
+
cmd.addCommand(suiteInitCommand());
|
|
26
|
+
cmd.addCommand(suiteValidateCommand());
|
|
27
|
+
return cmd;
|
|
28
|
+
}
|
|
29
|
+
function suiteInitCommand() {
|
|
30
|
+
const cmd = new Command("init");
|
|
31
|
+
cmd.description("Scaffold a new .sdtsuite file from a list of project paths.").requiredOption("--out <path>", "Where to write the new .sdtsuite file.").requiredOption("--name <suite-name>", "Suite display name.").option(
|
|
32
|
+
"--project <alias=path>",
|
|
33
|
+
"Add a project to the suite (repeatable). Format: alias=path/to/Project.sdtproj",
|
|
34
|
+
(value, prev) => {
|
|
35
|
+
const m = /^([A-Za-z_][\w-]*)=(.+)$/.exec(value);
|
|
36
|
+
if (!m) throw new Error(`Bad --project value "${value}". Expected alias=path.`);
|
|
37
|
+
return [...prev, { alias: m[1], path: m[2] }];
|
|
38
|
+
},
|
|
39
|
+
[]
|
|
40
|
+
).action(async (opts) => {
|
|
41
|
+
await warnIfMissingLicense("suite");
|
|
42
|
+
const projects = opts.project;
|
|
43
|
+
if (projects.length === 0) {
|
|
44
|
+
throw new Error("At least one --project alias=path is required.");
|
|
45
|
+
}
|
|
46
|
+
const outPath = path.resolve(String(opts.out));
|
|
47
|
+
const suite = newSuiteTemplate(String(opts.name), projects);
|
|
48
|
+
await saveSuite(outPath, suite);
|
|
49
|
+
logger.success(`Wrote suite: ${outPath} (${projects.length} project(s))`);
|
|
50
|
+
});
|
|
51
|
+
return cmd;
|
|
52
|
+
}
|
|
53
|
+
function suiteValidateCommand() {
|
|
54
|
+
const cmd = new Command("validate");
|
|
55
|
+
cmd.description("Validate a .sdtsuite: ownership conflicts, refs, cycles, deploy order.").argument("<suite>", "Path to a .sdtsuite file.").option("--json", "Emit machine-readable JSON.", false).action(async (suiteArg, opts) => {
|
|
56
|
+
await warnIfMissingLicense("suite");
|
|
57
|
+
const loaded = await loadSuite(suiteArg);
|
|
58
|
+
const result = validateSuite(loaded);
|
|
59
|
+
if (opts.json) {
|
|
60
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
61
|
+
} else {
|
|
62
|
+
printSuiteValidation(loaded.suite.suiteName, result.findings, result.deployOrder);
|
|
63
|
+
}
|
|
64
|
+
const errors = result.findings.filter((f) => f.severity === "ERROR").length;
|
|
65
|
+
if (errors > 0) process.exitCode = 2;
|
|
66
|
+
});
|
|
67
|
+
return cmd;
|
|
68
|
+
}
|
|
69
|
+
function printSuiteValidation(suiteName, findings, deployOrder) {
|
|
70
|
+
logger.info(`Suite: ${suiteName}`);
|
|
71
|
+
const errors = findings.filter((f) => f.severity === "ERROR");
|
|
72
|
+
const warnings = findings.filter((f) => f.severity === "WARNING");
|
|
73
|
+
const infos = findings.filter((f) => f.severity === "INFO");
|
|
74
|
+
logger.info(` ${errors.length} error(s), ${warnings.length} warning(s), ${infos.length} info`);
|
|
75
|
+
for (const f of [...errors, ...warnings, ...infos]) {
|
|
76
|
+
const tag = f.severity === "ERROR" ? "ERROR" : f.severity === "WARNING" ? "WARN " : "INFO ";
|
|
77
|
+
logger.info(` [${tag}] ${f.code}: ${f.message}`);
|
|
78
|
+
}
|
|
79
|
+
if (deployOrder.length > 0) {
|
|
80
|
+
logger.info(` Deploy order: ${deployOrder.join(" \u2192 ")}`);
|
|
81
|
+
} else {
|
|
82
|
+
logger.error(` Suite is undeployable (cycle or invalid order).`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
suiteCommand
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=suite-TRNGZWQM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/suite.ts"],"sourcesContent":["import path from 'node:path';\nimport { Command } from 'commander';\nimport {\n loadSuite,\n newSuiteTemplate,\n saveSuite,\n validateSuite,\n type SuiteFinding,\n} from '@sdt-tools/core/project';\nimport { license as licenseApi } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\n\nasync function warnIfMissingLicense(feature: licenseApi.ProFeature): Promise<void> {\n const lic = await licenseApi.loadLicense();\n const r = licenseApi.checkProFeature(lic, feature);\n if (r.warning) logger.warn(`[Pro] ${r.warning}`);\n}\n\n/**\n * `sdt suite` — manage and validate a `.sdtsuite` file, the collection of\n * `.sdtproj` projects that compose into one Snowflake target.\n *\n * Subcommands:\n * init — scaffold a new .sdtsuite from a list of project paths\n * validate — run the 5 suite-level checks (ownership, refs, cycles,\n * coverage, deploy order)\n * compare — alias for `sdt compare`, run on each suite project in\n * deploy order (forwards each compare to a target)\n * publish — alias for `sdt publish`, run on each suite project in\n * deploy order (sequential, fail-fast)\n *\n * Pro-tier feature.\n */\nexport function suiteCommand(): Command {\n const cmd = new Command('suite').description(\n 'Manage a .sdtsuite — a collection of .sdtproj projects deploying together.',\n );\n cmd.addCommand(suiteInitCommand());\n cmd.addCommand(suiteValidateCommand());\n return cmd;\n}\n\nfunction suiteInitCommand(): Command {\n const cmd = new Command('init');\n cmd\n .description('Scaffold a new .sdtsuite file from a list of project paths.')\n .requiredOption('--out <path>', 'Where to write the new .sdtsuite file.')\n .requiredOption('--name <suite-name>', 'Suite display name.')\n .option(\n '--project <alias=path>',\n 'Add a project to the suite (repeatable). Format: alias=path/to/Project.sdtproj',\n (value: string, prev: Array<{ alias: string; path: string }>) => {\n const m = /^([A-Za-z_][\\w-]*)=(.+)$/.exec(value);\n if (!m) throw new Error(`Bad --project value \"${value}\". Expected alias=path.`);\n return [...prev, { alias: m[1]!, path: m[2]! }];\n },\n [] as Array<{ alias: string; path: string }>,\n )\n .action(async (opts) => {\n await warnIfMissingLicense('suite');\n const projects = opts.project as Array<{ alias: string; path: string }>;\n if (projects.length === 0) {\n throw new Error('At least one --project alias=path is required.');\n }\n const outPath = path.resolve(String(opts.out));\n const suite = newSuiteTemplate(String(opts.name), projects);\n await saveSuite(outPath, suite);\n logger.success(`Wrote suite: ${outPath} (${projects.length} project(s))`);\n });\n return cmd;\n}\n\nfunction suiteValidateCommand(): Command {\n const cmd = new Command('validate');\n cmd\n .description('Validate a .sdtsuite: ownership conflicts, refs, cycles, deploy order.')\n .argument('<suite>', 'Path to a .sdtsuite file.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (suiteArg: string, opts) => {\n await warnIfMissingLicense('suite');\n const loaded = await loadSuite(suiteArg);\n const result = validateSuite(loaded);\n if (opts.json) {\n process.stdout.write(JSON.stringify(result, null, 2) + '\\n');\n } else {\n printSuiteValidation(loaded.suite.suiteName, result.findings, result.deployOrder);\n }\n const errors = result.findings.filter((f) => f.severity === 'ERROR').length;\n if (errors > 0) process.exitCode = 2;\n });\n return cmd;\n}\n\nfunction printSuiteValidation(\n suiteName: string,\n findings: SuiteFinding[],\n deployOrder: string[],\n): void {\n logger.info(`Suite: ${suiteName}`);\n const errors = findings.filter((f) => f.severity === 'ERROR');\n const warnings = findings.filter((f) => f.severity === 'WARNING');\n const infos = findings.filter((f) => f.severity === 'INFO');\n logger.info(` ${errors.length} error(s), ${warnings.length} warning(s), ${infos.length} info`);\n for (const f of [...errors, ...warnings, ...infos]) {\n const tag = f.severity === 'ERROR' ? 'ERROR' : f.severity === 'WARNING' ? 'WARN ' : 'INFO ';\n logger.info(` [${tag}] ${f.code}: ${f.message}`);\n }\n if (deployOrder.length > 0) {\n logger.info(` Deploy order: ${deployOrder.join(' → ')}`);\n } else {\n logger.error(` Suite is undeployable (cycle or invalid order).`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-VM2H4LAO.js";
|
|
4
|
+
import "./chunk-DGUM43GV.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/telemetry.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { errorReport, telemetry } from "@sdt-tools/core";
|
|
13
|
+
function prefPath() {
|
|
14
|
+
return process.env["SDT_TELEMETRY_PREF_PATH"] ?? join(homedir(), ".sdt", "telemetry.json");
|
|
15
|
+
}
|
|
16
|
+
function telemetryCommand() {
|
|
17
|
+
const cmd = new Command("telemetry").description(
|
|
18
|
+
"Control opt-in usage telemetry and automatic error reporting."
|
|
19
|
+
);
|
|
20
|
+
cmd.command("on").description("Enable telemetry + automatic error reporting.").action(async () => {
|
|
21
|
+
await writePref("on");
|
|
22
|
+
await errorReport.writeConsent("on");
|
|
23
|
+
logger.info("Automatic error reporting enabled.");
|
|
24
|
+
});
|
|
25
|
+
cmd.command("off").description("Disable telemetry + automatic error reporting.").action(async () => {
|
|
26
|
+
await writePref("off");
|
|
27
|
+
await errorReport.writeConsent("off");
|
|
28
|
+
logger.info("Automatic error reporting disabled.");
|
|
29
|
+
logger.warn(errorReport.CONSENT_WARNING);
|
|
30
|
+
});
|
|
31
|
+
cmd.command("status").description("Show current telemetry + error-reporting state.").action(async () => {
|
|
32
|
+
const stored = await readPref();
|
|
33
|
+
const enabled = telemetry.isTelemetryEnabled(stored.pref);
|
|
34
|
+
logger.info(`Stored preference: ${stored.pref}`);
|
|
35
|
+
logger.info(`Currently enabled: ${enabled}`);
|
|
36
|
+
if (process.env.CI === "true") logger.info("Detected CI=true \u2192 telemetry forced off.");
|
|
37
|
+
if (process.env.DO_NOT_TRACK === "1")
|
|
38
|
+
logger.info("Detected DO_NOT_TRACK=1 \u2192 telemetry forced off.");
|
|
39
|
+
const consent = await errorReport.readConsent();
|
|
40
|
+
const reportingEnabled = errorReport.isErrorReportingEnabled(consent.consent);
|
|
41
|
+
const spooled = await errorReport.listSpooled();
|
|
42
|
+
logger.info(
|
|
43
|
+
`Error reporting: ${consent.consent} (sending ${reportingEnabled ? "enabled" : "disabled"})`
|
|
44
|
+
);
|
|
45
|
+
if (spooled.length > 0) {
|
|
46
|
+
const occurrences = spooled.reduce((total, record) => total + record.count, 0);
|
|
47
|
+
logger.info(
|
|
48
|
+
`Locally captured errors: ${spooled.length} distinct (${occurrences} occurrences) awaiting ${reportingEnabled ? "upload" : "consent"}.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!reportingEnabled) logger.dim(` ${errorReport.CONSENT_WARNING}`);
|
|
52
|
+
});
|
|
53
|
+
return cmd;
|
|
54
|
+
}
|
|
55
|
+
async function writePref(pref) {
|
|
56
|
+
const file = prefPath();
|
|
57
|
+
await mkdir(dirname(file), { recursive: true });
|
|
58
|
+
const stored = { pref, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
59
|
+
await writeFile(file, JSON.stringify(stored, null, 2) + "\n", "utf8");
|
|
60
|
+
logger.info(`Telemetry preference set to "${pref}".`);
|
|
61
|
+
}
|
|
62
|
+
async function readPref() {
|
|
63
|
+
const file = prefPath();
|
|
64
|
+
if (!existsSync(file)) return { pref: "unset", updatedAt: "" };
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(file, "utf8");
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
return { pref: "unset", updatedAt: "" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
telemetryCommand
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=telemetry-3U2QLA2S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/telemetry.ts"],"sourcesContent":["/**\n * `sdt telemetry on|off|status` — opt-in / opt-out of usage telemetry AND\n * automatic error reporting (ERR.2: one consent governs both).\n *\n * Preference is stored in `~/.sdt/telemetry.json`; the error-reporting\n * consent in `~/.sdt/error-reporting.json`. Env vars (`DO_NOT_TRACK`,\n * `SDT_TELEMETRY=0`, `CI`) always win — they disable both regardless of\n * the stored preference. See docs/SECURITY.md for what's collected.\n */\nimport { Command } from 'commander';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\n\nimport { errorReport, telemetry } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\n\n/** Hermetic override for tests — mirrors `SDT_TRIAL_PATH` / `SDT_ERROR_CONSENT_PATH`. */\nfunction prefPath(): string {\n return process.env['SDT_TELEMETRY_PREF_PATH'] ?? join(homedir(), '.sdt', 'telemetry.json');\n}\n\ninterface StoredPreference {\n pref: 'on' | 'off' | 'unset';\n updatedAt: string;\n}\n\nexport function telemetryCommand(): Command {\n const cmd = new Command('telemetry').description(\n 'Control opt-in usage telemetry and automatic error reporting.',\n );\n\n cmd\n .command('on')\n .description('Enable telemetry + automatic error reporting.')\n .action(async () => {\n await writePref('on');\n await errorReport.writeConsent('on');\n logger.info('Automatic error reporting enabled.');\n });\n cmd\n .command('off')\n .description('Disable telemetry + automatic error reporting.')\n .action(async () => {\n await writePref('off');\n await errorReport.writeConsent('off');\n logger.info('Automatic error reporting disabled.');\n logger.warn(errorReport.CONSENT_WARNING);\n });\n cmd\n .command('status')\n .description('Show current telemetry + error-reporting state.')\n .action(async () => {\n const stored = await readPref();\n const enabled = telemetry.isTelemetryEnabled(stored.pref);\n logger.info(`Stored preference: ${stored.pref}`);\n logger.info(`Currently enabled: ${enabled}`);\n if (process.env.CI === 'true') logger.info('Detected CI=true → telemetry forced off.');\n if (process.env.DO_NOT_TRACK === '1')\n logger.info('Detected DO_NOT_TRACK=1 → telemetry forced off.');\n\n // ERR.2 — error-reporting consent + local spool state.\n const consent = await errorReport.readConsent();\n const reportingEnabled = errorReport.isErrorReportingEnabled(consent.consent);\n const spooled = await errorReport.listSpooled();\n logger.info(\n `Error reporting: ${consent.consent} (sending ${reportingEnabled ? 'enabled' : 'disabled'})`,\n );\n if (spooled.length > 0) {\n const occurrences = spooled.reduce((total, record) => total + record.count, 0);\n logger.info(\n `Locally captured errors: ${spooled.length} distinct (${occurrences} occurrences) awaiting ${reportingEnabled ? 'upload' : 'consent'}.`,\n );\n }\n if (!reportingEnabled) logger.dim(` ${errorReport.CONSENT_WARNING}`);\n });\n\n return cmd;\n}\n\nasync function writePref(pref: 'on' | 'off'): Promise<void> {\n const file = prefPath();\n await mkdir(dirname(file), { recursive: true });\n const stored: StoredPreference = { pref, updatedAt: new Date().toISOString() };\n await writeFile(file, JSON.stringify(stored, null, 2) + '\\n', 'utf8');\n logger.info(`Telemetry preference set to \"${pref}\".`);\n}\n\nasync function readPref(): Promise<StoredPreference> {\n const file = prefPath();\n if (!existsSync(file)) return { pref: 'unset', updatedAt: '' };\n try {\n const raw = await readFile(file, 'utf8');\n return JSON.parse(raw) as StoredPreference;\n } catch {\n return { pref: 'unset', updatedAt: '' };\n }\n}\n"],"mappings":";;;;;;AASA,SAAS,eAAe;AACxB,SAAS,OAAO,UAAU,iBAAiB;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AAExB,SAAS,aAAa,iBAAiB;AAIvC,SAAS,WAAmB;AAC1B,SAAO,QAAQ,IAAI,yBAAyB,KAAK,KAAK,QAAQ,GAAG,QAAQ,gBAAgB;AAC3F;AAOO,SAAS,mBAA4B;AAC1C,QAAM,MAAM,IAAI,QAAQ,WAAW,EAAE;AAAA,IACnC;AAAA,EACF;AAEA,MACG,QAAQ,IAAI,EACZ,YAAY,+CAA+C,EAC3D,OAAO,YAAY;AAClB,UAAM,UAAU,IAAI;AACpB,UAAM,YAAY,aAAa,IAAI;AACnC,WAAO,KAAK,oCAAoC;AAAA,EAClD,CAAC;AACH,MACG,QAAQ,KAAK,EACb,YAAY,gDAAgD,EAC5D,OAAO,YAAY;AAClB,UAAM,UAAU,KAAK;AACrB,UAAM,YAAY,aAAa,KAAK;AACpC,WAAO,KAAK,qCAAqC;AACjD,WAAO,KAAK,YAAY,eAAe;AAAA,EACzC,CAAC;AACH,MACG,QAAQ,QAAQ,EAChB,YAAY,iDAAiD,EAC7D,OAAO,YAAY;AAClB,UAAM,SAAS,MAAM,SAAS;AAC9B,UAAM,UAAU,UAAU,mBAAmB,OAAO,IAAI;AACxD,WAAO,KAAK,sBAAsB,OAAO,IAAI,EAAE;AAC/C,WAAO,KAAK,sBAAsB,OAAO,EAAE;AAC3C,QAAI,QAAQ,IAAI,OAAO,OAAQ,QAAO,KAAK,+CAA0C;AACrF,QAAI,QAAQ,IAAI,iBAAiB;AAC/B,aAAO,KAAK,sDAAiD;AAG/D,UAAM,UAAU,MAAM,YAAY,YAAY;AAC9C,UAAM,mBAAmB,YAAY,wBAAwB,QAAQ,OAAO;AAC5E,UAAM,UAAU,MAAM,YAAY,YAAY;AAC9C,WAAO;AAAA,MACL,oBAAoB,QAAQ,OAAO,aAAa,mBAAmB,YAAY,UAAU;AAAA,IAC3F;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,cAAc,QAAQ,OAAO,CAAC,OAAO,WAAW,QAAQ,OAAO,OAAO,CAAC;AAC7E,aAAO;AAAA,QACL,4BAA4B,QAAQ,MAAM,cAAc,WAAW,0BAA0B,mBAAmB,WAAW,SAAS;AAAA,MACtI;AAAA,IACF;AACA,QAAI,CAAC,iBAAkB,QAAO,IAAI,KAAK,YAAY,eAAe,EAAE;AAAA,EACtE,CAAC;AAEH,SAAO;AACT;AAEA,eAAe,UAAU,MAAmC;AAC1D,QAAM,OAAO,SAAS;AACtB,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,SAA2B,EAAE,MAAM,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE;AAC7E,QAAM,UAAU,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,MAAM,MAAM;AACpE,SAAO,KAAK,gCAAgC,IAAI,IAAI;AACtD;AAEA,eAAe,WAAsC;AACnD,QAAM,OAAO,SAAS;AACtB,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,EAAE,MAAM,SAAS,WAAW,GAAG;AAC7D,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,MAAM,MAAM;AACvC,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO,EAAE,MAAM,SAAS,WAAW,GAAG;AAAA,EACxC;AACF;","names":[]}
|