@decantr/cli 1.11.0 → 2.1.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/bin.js +3 -3
- package/dist/{chunk-5RODH77L.js → chunk-5QM6XDZU.js} +3228 -3076
- package/dist/{chunk-6YCFRZZI.js → chunk-KGEEYXSU.js} +42 -3
- package/dist/{chunk-USOO77A5.js → chunk-WDA4SHIQ.js} +190 -283
- package/dist/{chunk-RSDCWAHD.js → chunk-X2HIXQAY.js} +9 -6
- package/dist/{chunk-DI2PLOJ6.js → chunk-ZUUJ24YU.js} +418 -180
- package/dist/{heal-5JHGCLDX.js → heal-MQ56WYX4.js} +2 -2
- package/dist/{health-3TJYYTX6.js → health-DCT625XN.js} +3 -3
- package/dist/index.js +3 -3
- package/dist/{studio-7TE7YXFG.js → studio-CI7OOGHV.js} +21 -4
- package/dist/{upgrade-4NRDVD5N.js → upgrade-PL755AF7.js} +21 -41
- package/package.json +6 -6
|
@@ -1,139 +1,8 @@
|
|
|
1
|
-
// src/lib/scan-interactions.ts
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
3
|
-
import { extname, join } from "path";
|
|
4
|
-
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
5
|
-
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
6
|
-
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
7
|
-
"node_modules",
|
|
8
|
-
".decantr",
|
|
9
|
-
".git",
|
|
10
|
-
"dist",
|
|
11
|
-
"build",
|
|
12
|
-
".next",
|
|
13
|
-
".turbo",
|
|
14
|
-
"coverage",
|
|
15
|
-
".cache"
|
|
16
|
-
]);
|
|
17
|
-
var MAX_FILE_SIZE = 1024 * 1024;
|
|
18
|
-
function walkSourceTree(rootDir) {
|
|
19
|
-
const sources = /* @__PURE__ */ new Map();
|
|
20
|
-
function walk2(dir) {
|
|
21
|
-
let entries;
|
|
22
|
-
try {
|
|
23
|
-
entries = readdirSync(dir);
|
|
24
|
-
} catch {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
if (SKIP_DIRECTORIES.has(entry)) continue;
|
|
29
|
-
const fullPath = join(dir, entry);
|
|
30
|
-
let s;
|
|
31
|
-
try {
|
|
32
|
-
s = statSync(fullPath);
|
|
33
|
-
} catch {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (s.isDirectory()) {
|
|
37
|
-
walk2(fullPath);
|
|
38
|
-
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
|
|
39
|
-
if (s.size > MAX_FILE_SIZE) continue;
|
|
40
|
-
try {
|
|
41
|
-
sources.set(fullPath, readFileSync(fullPath, "utf8"));
|
|
42
|
-
} catch {
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
walk2(rootDir);
|
|
48
|
-
return sources;
|
|
49
|
-
}
|
|
50
|
-
function collectDeclaredInteractions(projectRoot) {
|
|
51
|
-
const manifestPath = join(projectRoot, ".decantr", "context", "pack-manifest.json");
|
|
52
|
-
if (!existsSync(manifestPath)) return [];
|
|
53
|
-
let manifest;
|
|
54
|
-
try {
|
|
55
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
56
|
-
} catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
const all = [];
|
|
60
|
-
const pages = manifest.pages ?? [];
|
|
61
|
-
const contextDir = join(projectRoot, ".decantr", "context");
|
|
62
|
-
for (const page of pages) {
|
|
63
|
-
const packPath = join(contextDir, page.json);
|
|
64
|
-
if (!existsSync(packPath)) continue;
|
|
65
|
-
let pack;
|
|
66
|
-
try {
|
|
67
|
-
pack = JSON.parse(readFileSync(packPath, "utf8"));
|
|
68
|
-
} catch {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const patterns = pack.data?.patterns ?? [];
|
|
72
|
-
for (const pat of patterns) {
|
|
73
|
-
if (Array.isArray(pat.interactions)) {
|
|
74
|
-
all.push(...pat.interactions);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return all;
|
|
79
|
-
}
|
|
80
|
-
function scanProjectInteractions(projectRoot) {
|
|
81
|
-
const declared = collectDeclaredInteractions(projectRoot);
|
|
82
|
-
if (declared.length === 0) return [];
|
|
83
|
-
const sources = walkSourceTree(projectRoot);
|
|
84
|
-
if (sources.size === 0) return [];
|
|
85
|
-
const missing = verifyInteractionsInSource(declared, sources);
|
|
86
|
-
return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// src/guard-context.ts
|
|
90
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
91
|
-
import { join as join2 } from "path";
|
|
92
|
-
function loadJsonEntries(dir) {
|
|
93
|
-
if (!existsSync2(dir)) return [];
|
|
94
|
-
try {
|
|
95
|
-
return readdirSync2(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
|
|
96
|
-
} catch {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
101
|
-
const themeRegistry = /* @__PURE__ */ new Map();
|
|
102
|
-
const patternRegistry = /* @__PURE__ */ new Map();
|
|
103
|
-
const cacheDir = join2(projectRoot, ".decantr", "cache");
|
|
104
|
-
const customDir = join2(projectRoot, ".decantr", "custom");
|
|
105
|
-
for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
|
|
106
|
-
if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
|
|
107
|
-
themeRegistry.set(data.id, {
|
|
108
|
-
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
for (const data of loadJsonEntries(join2(customDir, "themes"))) {
|
|
113
|
-
if (typeof data.id === "string") {
|
|
114
|
-
themeRegistry.set(`custom:${data.id}`, {
|
|
115
|
-
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
|
|
120
|
-
if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
|
|
121
|
-
patternRegistry.set(data.id, data);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
|
|
125
|
-
if (typeof data.id === "string") {
|
|
126
|
-
patternRegistry.set(data.id, data);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return { themeRegistry, patternRegistry };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
1
|
// src/telemetry.ts
|
|
133
2
|
import { randomUUID } from "crypto";
|
|
134
|
-
import { existsSync
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
135
4
|
import { homedir } from "os";
|
|
136
|
-
import { dirname, join
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
137
6
|
import { fileURLToPath } from "url";
|
|
138
7
|
import {
|
|
139
8
|
createFetchTelemetrySink,
|
|
@@ -159,21 +28,21 @@ async function sendGuardMetrics(metrics) {
|
|
|
159
28
|
}
|
|
160
29
|
}
|
|
161
30
|
function isOptedIn(projectRoot) {
|
|
162
|
-
const projectJsonPath =
|
|
163
|
-
if (!
|
|
31
|
+
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
32
|
+
if (!existsSync(projectJsonPath)) return false;
|
|
164
33
|
try {
|
|
165
|
-
const data = JSON.parse(
|
|
34
|
+
const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
166
35
|
return data.telemetry === true;
|
|
167
36
|
} catch {
|
|
168
37
|
return false;
|
|
169
38
|
}
|
|
170
39
|
}
|
|
171
40
|
function optIn(projectRoot) {
|
|
172
|
-
const projectJsonPath =
|
|
41
|
+
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
173
42
|
let data = {};
|
|
174
|
-
if (
|
|
43
|
+
if (existsSync(projectJsonPath)) {
|
|
175
44
|
try {
|
|
176
|
-
data = JSON.parse(
|
|
45
|
+
data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
177
46
|
} catch {
|
|
178
47
|
}
|
|
179
48
|
}
|
|
@@ -181,16 +50,19 @@ function optIn(projectRoot) {
|
|
|
181
50
|
mkdirSync(dirname(projectJsonPath), { recursive: true });
|
|
182
51
|
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
183
52
|
}
|
|
184
|
-
async function
|
|
185
|
-
const projectRoot =
|
|
186
|
-
|
|
187
|
-
|
|
53
|
+
async function captureCliTelemetryEvent(input) {
|
|
54
|
+
const projectRoot = resolveCliTelemetryProjectRoot(
|
|
55
|
+
input.projectRoot ?? process.cwd(),
|
|
56
|
+
input.args ?? []
|
|
57
|
+
);
|
|
58
|
+
if (!isOptedIn(projectRoot)) {
|
|
188
59
|
return;
|
|
189
60
|
}
|
|
190
61
|
const identities = ensureTelemetryIdentities(projectRoot);
|
|
191
62
|
if (!identities) {
|
|
192
63
|
return;
|
|
193
64
|
}
|
|
65
|
+
const registrySource = input.registrySource ?? getRegistrySourceProperty(input.properties) ?? inferRegistrySource(input.args ?? []);
|
|
194
66
|
const client = createTelemetryClient({
|
|
195
67
|
sink: createFetchTelemetrySink({
|
|
196
68
|
endpoint: getTelemetryEventsEndpoint(),
|
|
@@ -198,7 +70,7 @@ async function sendCliCommandTelemetry(input) {
|
|
|
198
70
|
})
|
|
199
71
|
});
|
|
200
72
|
const event = {
|
|
201
|
-
name:
|
|
73
|
+
name: input.name,
|
|
202
74
|
context: {
|
|
203
75
|
source: "cli",
|
|
204
76
|
actorType: getTelemetryActorType(),
|
|
@@ -206,26 +78,196 @@ async function sendCliCommandTelemetry(input) {
|
|
|
206
78
|
decantrVersion: getCliVersion(),
|
|
207
79
|
installId: identities.installId,
|
|
208
80
|
projectId: identities.projectId,
|
|
209
|
-
registrySource
|
|
81
|
+
registrySource
|
|
210
82
|
},
|
|
211
|
-
properties:
|
|
212
|
-
command,
|
|
213
|
-
success: input.success,
|
|
214
|
-
durationMs: input.durationMs,
|
|
215
|
-
adoptionMode: inferAdoptionMode(input.args),
|
|
216
|
-
errorCode: input.success ? void 0 : "cli_command_failed",
|
|
217
|
-
offline: input.args.includes("--offline"),
|
|
218
|
-
projectScope: inferProjectScope(projectRoot),
|
|
219
|
-
registrySource: inferRegistrySource(input.args),
|
|
220
|
-
targetFramework: inferFlagValue(input.args, "--target"),
|
|
221
|
-
workflowMode: inferWorkflowMode(input.args)
|
|
222
|
-
}
|
|
83
|
+
properties: input.properties
|
|
223
84
|
};
|
|
224
85
|
try {
|
|
225
86
|
await client.capture(event);
|
|
226
87
|
} catch {
|
|
227
88
|
}
|
|
228
89
|
}
|
|
90
|
+
async function sendCliCommandTelemetry(input) {
|
|
91
|
+
const projectRoot = resolveCliTelemetryProjectRoot(input.projectRoot ?? process.cwd(), input.args);
|
|
92
|
+
const command = normalizeCommand(input.args[0]);
|
|
93
|
+
if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version") {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const properties = buildCliLifecycleProperties({
|
|
97
|
+
args: input.args,
|
|
98
|
+
command,
|
|
99
|
+
durationMs: input.durationMs,
|
|
100
|
+
projectRoot,
|
|
101
|
+
success: input.success
|
|
102
|
+
});
|
|
103
|
+
await captureCliTelemetryEvent({
|
|
104
|
+
args: input.args,
|
|
105
|
+
name: "cli.command.completed",
|
|
106
|
+
projectRoot,
|
|
107
|
+
properties,
|
|
108
|
+
registrySource: properties.registrySource
|
|
109
|
+
});
|
|
110
|
+
const lifecycleEventName = lifecycleTelemetryEventName(command);
|
|
111
|
+
if (lifecycleEventName) {
|
|
112
|
+
await captureCliTelemetryEvent({
|
|
113
|
+
args: input.args,
|
|
114
|
+
name: lifecycleEventName,
|
|
115
|
+
projectRoot,
|
|
116
|
+
properties,
|
|
117
|
+
registrySource: properties.registrySource
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function sendProjectHealthReportTelemetry(input) {
|
|
122
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
123
|
+
const properties = buildProjectHealthTelemetryProperties(input, projectRoot);
|
|
124
|
+
await captureCliTelemetryEvent({
|
|
125
|
+
name: "health.report.generated",
|
|
126
|
+
projectRoot,
|
|
127
|
+
properties
|
|
128
|
+
});
|
|
129
|
+
if (input.report.status === "healthy") {
|
|
130
|
+
await captureCliTelemetryEvent({
|
|
131
|
+
name: "decantr.health.healthy",
|
|
132
|
+
projectRoot,
|
|
133
|
+
properties
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function sendNewProjectCompletedTelemetry(input) {
|
|
138
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
139
|
+
const args = input.args ?? ["new"];
|
|
140
|
+
const base = buildCliLifecycleProperties({
|
|
141
|
+
args,
|
|
142
|
+
command: "new",
|
|
143
|
+
durationMs: input.durationMs,
|
|
144
|
+
projectRoot,
|
|
145
|
+
success: input.success
|
|
146
|
+
});
|
|
147
|
+
const properties = {
|
|
148
|
+
...base,
|
|
149
|
+
command: "new"
|
|
150
|
+
};
|
|
151
|
+
await captureCliTelemetryEvent({
|
|
152
|
+
args,
|
|
153
|
+
name: "decantr.new.completed",
|
|
154
|
+
projectRoot,
|
|
155
|
+
properties,
|
|
156
|
+
registrySource: properties.registrySource
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async function sendProjectHealthPromptTelemetry(input) {
|
|
160
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
161
|
+
const finding = input.finding;
|
|
162
|
+
const properties = {
|
|
163
|
+
success: Boolean(finding),
|
|
164
|
+
findingFound: Boolean(finding),
|
|
165
|
+
adoptionMode: normalizeAdoptionMode(input.report.summary.adoptionMode),
|
|
166
|
+
ci: input.ci ?? false,
|
|
167
|
+
findingSeverity: normalizeFindingSeverity(finding?.severity),
|
|
168
|
+
findingSource: normalizeFindingSource(finding?.source),
|
|
169
|
+
projectScope: inferProjectScope(projectRoot),
|
|
170
|
+
workflowMode: normalizeWorkflowMode(input.report.summary.workflowMode)
|
|
171
|
+
};
|
|
172
|
+
await captureCliTelemetryEvent({
|
|
173
|
+
name: "health.finding.prompt_requested",
|
|
174
|
+
projectRoot,
|
|
175
|
+
properties
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function sendProjectHealthCiFailedTelemetry(input) {
|
|
179
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
180
|
+
const properties = {
|
|
181
|
+
...buildProjectHealthTelemetryProperties(input, projectRoot),
|
|
182
|
+
errorCode: "project_health_ci_failed",
|
|
183
|
+
failOn: input.failOn,
|
|
184
|
+
success: false
|
|
185
|
+
};
|
|
186
|
+
await captureCliTelemetryEvent({
|
|
187
|
+
name: "health.ci.failed",
|
|
188
|
+
projectRoot,
|
|
189
|
+
properties
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function sendStudioStartedTelemetry(input) {
|
|
193
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
194
|
+
const metadata = readProjectTelemetryMetadata(projectRoot);
|
|
195
|
+
const properties = {
|
|
196
|
+
success: true,
|
|
197
|
+
hostMode: isLoopbackHost(input.host) ? "loopback" : "custom",
|
|
198
|
+
port: input.port,
|
|
199
|
+
adoptionMode: metadata.adoptionMode,
|
|
200
|
+
projectScope: inferProjectScope(projectRoot),
|
|
201
|
+
workflowMode: metadata.workflowMode
|
|
202
|
+
};
|
|
203
|
+
await captureCliTelemetryEvent({
|
|
204
|
+
name: "studio.started",
|
|
205
|
+
projectRoot,
|
|
206
|
+
properties
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function sendStudioHealthRefreshedTelemetry(input) {
|
|
210
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
211
|
+
const properties = {
|
|
212
|
+
...buildProjectHealthTelemetryProperties(input, projectRoot),
|
|
213
|
+
trigger: input.trigger ?? "api-refresh"
|
|
214
|
+
};
|
|
215
|
+
await captureCliTelemetryEvent({
|
|
216
|
+
name: "studio.health_refreshed",
|
|
217
|
+
projectRoot,
|
|
218
|
+
properties
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function buildCliLifecycleProperties(input) {
|
|
222
|
+
const metadata = readProjectTelemetryMetadata(input.projectRoot);
|
|
223
|
+
const registrySource = inferRegistrySource(input.args);
|
|
224
|
+
return {
|
|
225
|
+
command: input.command,
|
|
226
|
+
success: input.success,
|
|
227
|
+
durationMs: input.durationMs,
|
|
228
|
+
adoptionMode: inferAdoptionMode(input.args) ?? metadata.adoptionMode,
|
|
229
|
+
errorCode: input.success ? void 0 : "cli_command_failed",
|
|
230
|
+
offline: input.args.includes("--offline"),
|
|
231
|
+
projectScope: metadata.projectScope ?? inferProjectScope(input.projectRoot),
|
|
232
|
+
registrySource,
|
|
233
|
+
targetFramework: inferFlagValue(input.args, "--target"),
|
|
234
|
+
workflowMode: inferWorkflowMode(input.args) ?? metadata.workflowMode
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function lifecycleTelemetryEventName(command) {
|
|
238
|
+
if (command === "check") return "decantr.check.completed";
|
|
239
|
+
if (command === "init") return "decantr.init.completed";
|
|
240
|
+
if (command === "refresh") return "decantr.refresh.completed";
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function buildProjectHealthTelemetryProperties(input, projectRoot) {
|
|
244
|
+
const { report } = input;
|
|
245
|
+
return {
|
|
246
|
+
success: true,
|
|
247
|
+
status: report.status,
|
|
248
|
+
score: report.score,
|
|
249
|
+
durationMs: input.durationMs,
|
|
250
|
+
adoptionMode: normalizeAdoptionMode(report.summary.adoptionMode),
|
|
251
|
+
ci: input.ci ?? false,
|
|
252
|
+
errorCount: report.summary.errorCount,
|
|
253
|
+
failOn: input.failOn,
|
|
254
|
+
findingCount: report.summary.findingCount,
|
|
255
|
+
format: input.format,
|
|
256
|
+
infoCount: report.summary.infoCount,
|
|
257
|
+
outputWritten: input.outputWritten ?? false,
|
|
258
|
+
packManifestPresent: report.summary.packManifestPresent,
|
|
259
|
+
pageCount: report.summary.pageCount,
|
|
260
|
+
projectScope: inferProjectScope(projectRoot),
|
|
261
|
+
reviewPackPresent: report.summary.reviewPackPresent,
|
|
262
|
+
routeCount: report.routes.declared.length,
|
|
263
|
+
runtimeAuditChecked: report.summary.runtimeAuditChecked,
|
|
264
|
+
runtimeMatchedCount: report.routes.runtimeMatched,
|
|
265
|
+
runtimePassed: report.summary.runtimePassed,
|
|
266
|
+
runtimeRouteCheckedCount: report.routes.runtimeChecked.length,
|
|
267
|
+
warnCount: report.summary.warnCount,
|
|
268
|
+
workflowMode: normalizeWorkflowMode(report.summary.workflowMode)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
229
271
|
function collectMetrics(essence, issues) {
|
|
230
272
|
const dna = essence.dna ?? {};
|
|
231
273
|
const blueprint = essence.blueprint ?? {};
|
|
@@ -263,12 +305,12 @@ function collectMetrics(essence, issues) {
|
|
|
263
305
|
}
|
|
264
306
|
function ensureTelemetryIdentities(projectRoot) {
|
|
265
307
|
const installId = getOrCreateInstallId();
|
|
266
|
-
const projectJsonPath =
|
|
267
|
-
if (!
|
|
308
|
+
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
309
|
+
if (!existsSync(projectJsonPath)) {
|
|
268
310
|
return null;
|
|
269
311
|
}
|
|
270
312
|
try {
|
|
271
|
-
const data = JSON.parse(
|
|
313
|
+
const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
272
314
|
let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
|
|
273
315
|
if (!projectId) {
|
|
274
316
|
projectId = `project_${randomUUID()}`;
|
|
@@ -282,10 +324,10 @@ function ensureTelemetryIdentities(projectRoot) {
|
|
|
282
324
|
}
|
|
283
325
|
function getOrCreateInstallId() {
|
|
284
326
|
const configDir = getConfigDir();
|
|
285
|
-
const configPath =
|
|
327
|
+
const configPath = join(configDir, "config.json");
|
|
286
328
|
try {
|
|
287
|
-
if (
|
|
288
|
-
const data = JSON.parse(
|
|
329
|
+
if (existsSync(configPath)) {
|
|
330
|
+
const data = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
289
331
|
if (typeof data.telemetryInstallId === "string") {
|
|
290
332
|
return data.telemetryInstallId;
|
|
291
333
|
}
|
|
@@ -307,7 +349,7 @@ function getOrCreateInstallId() {
|
|
|
307
349
|
}
|
|
308
350
|
}
|
|
309
351
|
function getConfigDir() {
|
|
310
|
-
return process.env.DECANTR_CONFIG_DIR ||
|
|
352
|
+
return process.env.DECANTR_CONFIG_DIR || join(homedir(), ".config", "decantr");
|
|
311
353
|
}
|
|
312
354
|
function getTelemetryEventsEndpoint() {
|
|
313
355
|
return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
|
|
@@ -316,6 +358,37 @@ function getTelemetryActorType() {
|
|
|
316
358
|
const configured = process.env.DECANTR_TELEMETRY_ACTOR_TYPE;
|
|
317
359
|
return isTelemetryActorType(configured) ? configured : "customer";
|
|
318
360
|
}
|
|
361
|
+
function getRegistrySourceProperty(properties) {
|
|
362
|
+
const value = properties.registrySource;
|
|
363
|
+
return isRegistrySource(value) ? value : void 0;
|
|
364
|
+
}
|
|
365
|
+
function readProjectTelemetryMetadata(projectRoot) {
|
|
366
|
+
const data = readProjectJson(projectRoot);
|
|
367
|
+
const initialized = isRecord(data?.initialized) ? data.initialized : void 0;
|
|
368
|
+
return {
|
|
369
|
+
adoptionMode: normalizeAdoptionMode(initialized?.adoptionMode),
|
|
370
|
+
projectScope: normalizeProjectScope(initialized?.projectScope),
|
|
371
|
+
workflowMode: normalizeWorkflowMode(initialized?.workflowMode)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function resolveCliTelemetryProjectRoot(projectRoot, args) {
|
|
375
|
+
const projectFlag = inferFlagValue(args, "--project");
|
|
376
|
+
if (!projectFlag) return projectRoot;
|
|
377
|
+
const candidate = resolve(projectRoot, projectFlag);
|
|
378
|
+
return existsSync(join(candidate, ".decantr", "project.json")) ? candidate : projectRoot;
|
|
379
|
+
}
|
|
380
|
+
function readProjectJson(projectRoot) {
|
|
381
|
+
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
382
|
+
if (!existsSync(projectJsonPath)) return null;
|
|
383
|
+
try {
|
|
384
|
+
return JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function isRecord(value) {
|
|
390
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
391
|
+
}
|
|
319
392
|
function normalizeCommand(command) {
|
|
320
393
|
if (!command) return null;
|
|
321
394
|
if (command === "--help" || command === "-h") return "help";
|
|
@@ -336,13 +409,19 @@ function inferFlagValue(args, flag) {
|
|
|
336
409
|
}
|
|
337
410
|
function inferAdoptionMode(args) {
|
|
338
411
|
const value = inferFlagValue(args, "--adoption");
|
|
412
|
+
return normalizeAdoptionMode(value);
|
|
413
|
+
}
|
|
414
|
+
function inferWorkflowMode(args) {
|
|
415
|
+
const value = inferFlagValue(args, "--workflow");
|
|
416
|
+
return normalizeWorkflowMode(value);
|
|
417
|
+
}
|
|
418
|
+
function normalizeAdoptionMode(value) {
|
|
339
419
|
if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
|
|
340
420
|
return value;
|
|
341
421
|
}
|
|
342
422
|
return void 0;
|
|
343
423
|
}
|
|
344
|
-
function
|
|
345
|
-
const value = inferFlagValue(args, "--workflow");
|
|
424
|
+
function normalizeWorkflowMode(value) {
|
|
346
425
|
if (value === "greenfield" || value === "greenfield-scaffold") {
|
|
347
426
|
return "greenfield-scaffold";
|
|
348
427
|
}
|
|
@@ -357,6 +436,20 @@ function inferWorkflowMode(args) {
|
|
|
357
436
|
}
|
|
358
437
|
return void 0;
|
|
359
438
|
}
|
|
439
|
+
function normalizeProjectScope(value) {
|
|
440
|
+
if (value === "single-app" || value === "workspace-app") return value;
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
function normalizeFindingSeverity(value) {
|
|
444
|
+
if (value === "error" || value === "info" || value === "warn") return value;
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
function normalizeFindingSource(value) {
|
|
448
|
+
if (value === "audit" || value === "brownfield" || value === "check" || value === "interaction" || value === "pack" || value === "runtime") {
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
360
453
|
function inferRegistrySource(args) {
|
|
361
454
|
if (args.includes("--offline")) {
|
|
362
455
|
return "cache";
|
|
@@ -366,16 +459,19 @@ function inferRegistrySource(args) {
|
|
|
366
459
|
}
|
|
367
460
|
return "official";
|
|
368
461
|
}
|
|
462
|
+
function isRegistrySource(value) {
|
|
463
|
+
return value === "cache" || value === "custom" || value === "none" || value === "official" || value === "private";
|
|
464
|
+
}
|
|
369
465
|
function inferProjectScope(projectRoot) {
|
|
370
|
-
return
|
|
466
|
+
return existsSync(join(projectRoot, "pnpm-workspace.yaml")) || existsSync(join(projectRoot, "turbo.json")) || existsSync(join(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
371
467
|
}
|
|
372
468
|
function getCliVersion() {
|
|
373
469
|
try {
|
|
374
470
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
375
|
-
const candidates = [
|
|
471
|
+
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
376
472
|
for (const candidate of candidates) {
|
|
377
|
-
if (
|
|
378
|
-
const pkg = JSON.parse(
|
|
473
|
+
if (existsSync(candidate)) {
|
|
474
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
379
475
|
if (pkg.version) {
|
|
380
476
|
return pkg.version;
|
|
381
477
|
}
|
|
@@ -385,6 +481,140 @@ function getCliVersion() {
|
|
|
385
481
|
}
|
|
386
482
|
return "unknown";
|
|
387
483
|
}
|
|
484
|
+
function isLoopbackHost(host) {
|
|
485
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/guard-context.ts
|
|
489
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
490
|
+
import { join as join2 } from "path";
|
|
491
|
+
function loadJsonEntries(dir) {
|
|
492
|
+
if (!existsSync2(dir)) return [];
|
|
493
|
+
try {
|
|
494
|
+
return readdirSync(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
|
|
495
|
+
} catch {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
500
|
+
const themeRegistry = /* @__PURE__ */ new Map();
|
|
501
|
+
const patternRegistry = /* @__PURE__ */ new Map();
|
|
502
|
+
const cacheDir = join2(projectRoot, ".decantr", "cache");
|
|
503
|
+
const customDir = join2(projectRoot, ".decantr", "custom");
|
|
504
|
+
for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
|
|
505
|
+
if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
|
|
506
|
+
themeRegistry.set(data.id, {
|
|
507
|
+
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const data of loadJsonEntries(join2(customDir, "themes"))) {
|
|
512
|
+
if (typeof data.id === "string") {
|
|
513
|
+
themeRegistry.set(`custom:${data.id}`, {
|
|
514
|
+
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
|
|
519
|
+
if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
|
|
520
|
+
patternRegistry.set(data.id, data);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
|
|
524
|
+
if (typeof data.id === "string") {
|
|
525
|
+
patternRegistry.set(data.id, data);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { themeRegistry, patternRegistry };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/lib/scan-interactions.ts
|
|
532
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
|
|
533
|
+
import { extname, join as join3 } from "path";
|
|
534
|
+
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
535
|
+
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
536
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
537
|
+
"node_modules",
|
|
538
|
+
".decantr",
|
|
539
|
+
".git",
|
|
540
|
+
"dist",
|
|
541
|
+
"build",
|
|
542
|
+
".next",
|
|
543
|
+
".turbo",
|
|
544
|
+
"coverage",
|
|
545
|
+
".cache"
|
|
546
|
+
]);
|
|
547
|
+
var MAX_FILE_SIZE = 1024 * 1024;
|
|
548
|
+
function walkSourceTree(rootDir) {
|
|
549
|
+
const sources = /* @__PURE__ */ new Map();
|
|
550
|
+
function walk2(dir) {
|
|
551
|
+
let entries;
|
|
552
|
+
try {
|
|
553
|
+
entries = readdirSync2(dir);
|
|
554
|
+
} catch {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
for (const entry of entries) {
|
|
558
|
+
if (SKIP_DIRECTORIES.has(entry)) continue;
|
|
559
|
+
const fullPath = join3(dir, entry);
|
|
560
|
+
let s;
|
|
561
|
+
try {
|
|
562
|
+
s = statSync(fullPath);
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (s.isDirectory()) {
|
|
567
|
+
walk2(fullPath);
|
|
568
|
+
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
|
|
569
|
+
if (s.size > MAX_FILE_SIZE) continue;
|
|
570
|
+
try {
|
|
571
|
+
sources.set(fullPath, readFileSync3(fullPath, "utf8"));
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
walk2(rootDir);
|
|
578
|
+
return sources;
|
|
579
|
+
}
|
|
580
|
+
function collectDeclaredInteractions(projectRoot) {
|
|
581
|
+
const manifestPath = join3(projectRoot, ".decantr", "context", "pack-manifest.json");
|
|
582
|
+
if (!existsSync3(manifestPath)) return [];
|
|
583
|
+
let manifest;
|
|
584
|
+
try {
|
|
585
|
+
manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
586
|
+
} catch {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
const all = [];
|
|
590
|
+
const pages = manifest.pages ?? [];
|
|
591
|
+
const contextDir = join3(projectRoot, ".decantr", "context");
|
|
592
|
+
for (const page of pages) {
|
|
593
|
+
const packPath = join3(contextDir, page.json);
|
|
594
|
+
if (!existsSync3(packPath)) continue;
|
|
595
|
+
let pack;
|
|
596
|
+
try {
|
|
597
|
+
pack = JSON.parse(readFileSync3(packPath, "utf8"));
|
|
598
|
+
} catch {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const patterns = pack.data?.patterns ?? [];
|
|
602
|
+
for (const pat of patterns) {
|
|
603
|
+
if (Array.isArray(pat.interactions)) {
|
|
604
|
+
all.push(...pat.interactions);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return all;
|
|
609
|
+
}
|
|
610
|
+
function scanProjectInteractions(projectRoot) {
|
|
611
|
+
const declared = collectDeclaredInteractions(projectRoot);
|
|
612
|
+
if (declared.length === 0) return [];
|
|
613
|
+
const sources = walkSourceTree(projectRoot);
|
|
614
|
+
if (sources.size === 0) return [];
|
|
615
|
+
const missing = verifyInteractionsInSource(declared, sources);
|
|
616
|
+
return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
|
|
617
|
+
}
|
|
388
618
|
|
|
389
619
|
// src/analyzers/routes.ts
|
|
390
620
|
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
@@ -1158,7 +1388,9 @@ function readSmallText(projectRoot, relPath) {
|
|
|
1158
1388
|
}
|
|
1159
1389
|
}
|
|
1160
1390
|
function detectConflicts(projectRoot, items) {
|
|
1161
|
-
const text = items.filter(
|
|
1391
|
+
const text = items.filter(
|
|
1392
|
+
(item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)
|
|
1393
|
+
).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
|
|
1162
1394
|
const conflicts = [];
|
|
1163
1395
|
const frameworkSignals = [
|
|
1164
1396
|
["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
|
|
@@ -1172,9 +1404,7 @@ function detectConflicts(projectRoot, items) {
|
|
|
1172
1404
|
);
|
|
1173
1405
|
}
|
|
1174
1406
|
const forbidsTailwind = /\b(do not|don't|avoid|forbid|forbidden)\s+use\s+tailwind\b|\bno\s+tailwind\b/.test(text);
|
|
1175
|
-
const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(
|
|
1176
|
-
text
|
|
1177
|
-
);
|
|
1407
|
+
const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(text);
|
|
1178
1408
|
if (forbidsTailwind && endorsesTailwind) {
|
|
1179
1409
|
conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
|
|
1180
1410
|
}
|
|
@@ -1190,9 +1420,9 @@ function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
|
1190
1420
|
try {
|
|
1191
1421
|
const essence = JSON.parse(content);
|
|
1192
1422
|
const risks = [];
|
|
1193
|
-
if (essence.version !== "
|
|
1423
|
+
if (essence.version !== "4.0.0") {
|
|
1194
1424
|
risks.push(
|
|
1195
|
-
`decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; migrate or review before treating it as current brownfield doctrine.`
|
|
1425
|
+
`decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; run decantr migrate --to v4 or review before treating it as current brownfield doctrine.`
|
|
1196
1426
|
);
|
|
1197
1427
|
}
|
|
1198
1428
|
if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
|
|
@@ -1210,7 +1440,9 @@ function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
|
1210
1440
|
function detectStaleRisks(projectRoot, items) {
|
|
1211
1441
|
const pathRisks = items.filter(
|
|
1212
1442
|
(item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
|
|
1213
|
-
).slice(0, 12).map(
|
|
1443
|
+
).slice(0, 12).map(
|
|
1444
|
+
(item) => `${item.path} may be historical; verify before treating it as current doctrine.`
|
|
1445
|
+
);
|
|
1214
1446
|
return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
|
|
1215
1447
|
}
|
|
1216
1448
|
function scanAmbientContext(projectRoot) {
|
|
@@ -1402,17 +1634,23 @@ function readDoctrineMap(projectRoot) {
|
|
|
1402
1634
|
}
|
|
1403
1635
|
|
|
1404
1636
|
export {
|
|
1405
|
-
scanProjectInteractions,
|
|
1406
1637
|
scanRoutes,
|
|
1407
1638
|
scanStyling,
|
|
1408
1639
|
scanAmbientContext,
|
|
1409
1640
|
createDoctrineMap,
|
|
1410
1641
|
writeDoctrineMap,
|
|
1411
1642
|
readDoctrineMap,
|
|
1412
|
-
buildGuardRegistryContext,
|
|
1413
1643
|
sendGuardMetrics,
|
|
1414
1644
|
isOptedIn,
|
|
1415
1645
|
optIn,
|
|
1416
1646
|
sendCliCommandTelemetry,
|
|
1417
|
-
|
|
1647
|
+
sendProjectHealthReportTelemetry,
|
|
1648
|
+
sendNewProjectCompletedTelemetry,
|
|
1649
|
+
sendProjectHealthPromptTelemetry,
|
|
1650
|
+
sendProjectHealthCiFailedTelemetry,
|
|
1651
|
+
sendStudioStartedTelemetry,
|
|
1652
|
+
sendStudioHealthRefreshedTelemetry,
|
|
1653
|
+
collectMetrics,
|
|
1654
|
+
buildGuardRegistryContext,
|
|
1655
|
+
scanProjectInteractions
|
|
1418
1656
|
};
|