@decantr/cli 1.11.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/bin.js +3 -3
- package/dist/{chunk-6YCFRZZI.js → chunk-DPFORHLL.js} +42 -3
- package/dist/{chunk-5RODH77L.js → chunk-GOX5EJ56.js} +3331 -3083
- package/dist/{chunk-DI2PLOJ6.js → chunk-JYEEXSUX.js} +423 -180
- package/dist/{chunk-RSDCWAHD.js → chunk-LLQCXOHK.js} +9 -6
- package/dist/{chunk-USOO77A5.js → chunk-WDA4SHIQ.js} +190 -283
- package/dist/{heal-5JHGCLDX.js → heal-NWQNJ6PU.js} +2 -2
- package/dist/{health-3TJYYTX6.js → health-WJJ55W3H.js} +3 -3
- package/dist/index.js +3 -3
- package/dist/{studio-7TE7YXFG.js → studio-FJNGWWRM.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" || isHelpOrVersionProbe(input.args)) {
|
|
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,12 +358,48 @@ 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";
|
|
322
395
|
if (command === "--version" || command === "-v") return "version";
|
|
323
396
|
return command;
|
|
324
397
|
}
|
|
398
|
+
function isHelpOrVersionProbe(args) {
|
|
399
|
+
if (args.some((arg) => arg === "--help" || arg === "-h")) return true;
|
|
400
|
+
if (args[1] === "help") return true;
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
325
403
|
function inferFlagValue(args, flag) {
|
|
326
404
|
const equalsPrefix = `${flag}=`;
|
|
327
405
|
const inline = args.find((arg) => arg.startsWith(equalsPrefix));
|
|
@@ -336,13 +414,19 @@ function inferFlagValue(args, flag) {
|
|
|
336
414
|
}
|
|
337
415
|
function inferAdoptionMode(args) {
|
|
338
416
|
const value = inferFlagValue(args, "--adoption");
|
|
417
|
+
return normalizeAdoptionMode(value);
|
|
418
|
+
}
|
|
419
|
+
function inferWorkflowMode(args) {
|
|
420
|
+
const value = inferFlagValue(args, "--workflow");
|
|
421
|
+
return normalizeWorkflowMode(value);
|
|
422
|
+
}
|
|
423
|
+
function normalizeAdoptionMode(value) {
|
|
339
424
|
if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
|
|
340
425
|
return value;
|
|
341
426
|
}
|
|
342
427
|
return void 0;
|
|
343
428
|
}
|
|
344
|
-
function
|
|
345
|
-
const value = inferFlagValue(args, "--workflow");
|
|
429
|
+
function normalizeWorkflowMode(value) {
|
|
346
430
|
if (value === "greenfield" || value === "greenfield-scaffold") {
|
|
347
431
|
return "greenfield-scaffold";
|
|
348
432
|
}
|
|
@@ -357,6 +441,20 @@ function inferWorkflowMode(args) {
|
|
|
357
441
|
}
|
|
358
442
|
return void 0;
|
|
359
443
|
}
|
|
444
|
+
function normalizeProjectScope(value) {
|
|
445
|
+
if (value === "single-app" || value === "workspace-app") return value;
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
448
|
+
function normalizeFindingSeverity(value) {
|
|
449
|
+
if (value === "error" || value === "info" || value === "warn") return value;
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
function normalizeFindingSource(value) {
|
|
453
|
+
if (value === "audit" || value === "brownfield" || value === "check" || value === "interaction" || value === "pack" || value === "runtime") {
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
360
458
|
function inferRegistrySource(args) {
|
|
361
459
|
if (args.includes("--offline")) {
|
|
362
460
|
return "cache";
|
|
@@ -366,16 +464,19 @@ function inferRegistrySource(args) {
|
|
|
366
464
|
}
|
|
367
465
|
return "official";
|
|
368
466
|
}
|
|
467
|
+
function isRegistrySource(value) {
|
|
468
|
+
return value === "cache" || value === "custom" || value === "none" || value === "official" || value === "private";
|
|
469
|
+
}
|
|
369
470
|
function inferProjectScope(projectRoot) {
|
|
370
|
-
return
|
|
471
|
+
return existsSync(join(projectRoot, "pnpm-workspace.yaml")) || existsSync(join(projectRoot, "turbo.json")) || existsSync(join(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
371
472
|
}
|
|
372
473
|
function getCliVersion() {
|
|
373
474
|
try {
|
|
374
475
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
375
|
-
const candidates = [
|
|
476
|
+
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
376
477
|
for (const candidate of candidates) {
|
|
377
|
-
if (
|
|
378
|
-
const pkg = JSON.parse(
|
|
478
|
+
if (existsSync(candidate)) {
|
|
479
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
379
480
|
if (pkg.version) {
|
|
380
481
|
return pkg.version;
|
|
381
482
|
}
|
|
@@ -385,6 +486,140 @@ function getCliVersion() {
|
|
|
385
486
|
}
|
|
386
487
|
return "unknown";
|
|
387
488
|
}
|
|
489
|
+
function isLoopbackHost(host) {
|
|
490
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/guard-context.ts
|
|
494
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
495
|
+
import { join as join2 } from "path";
|
|
496
|
+
function loadJsonEntries(dir) {
|
|
497
|
+
if (!existsSync2(dir)) return [];
|
|
498
|
+
try {
|
|
499
|
+
return readdirSync(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
|
|
500
|
+
} catch {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
505
|
+
const themeRegistry = /* @__PURE__ */ new Map();
|
|
506
|
+
const patternRegistry = /* @__PURE__ */ new Map();
|
|
507
|
+
const cacheDir = join2(projectRoot, ".decantr", "cache");
|
|
508
|
+
const customDir = join2(projectRoot, ".decantr", "custom");
|
|
509
|
+
for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
|
|
510
|
+
if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
|
|
511
|
+
themeRegistry.set(data.id, {
|
|
512
|
+
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const data of loadJsonEntries(join2(customDir, "themes"))) {
|
|
517
|
+
if (typeof data.id === "string") {
|
|
518
|
+
themeRegistry.set(`custom:${data.id}`, {
|
|
519
|
+
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
|
|
524
|
+
if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
|
|
525
|
+
patternRegistry.set(data.id, data);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
|
|
529
|
+
if (typeof data.id === "string") {
|
|
530
|
+
patternRegistry.set(data.id, data);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return { themeRegistry, patternRegistry };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/lib/scan-interactions.ts
|
|
537
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
|
|
538
|
+
import { extname, join as join3 } from "path";
|
|
539
|
+
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
540
|
+
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
541
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
542
|
+
"node_modules",
|
|
543
|
+
".decantr",
|
|
544
|
+
".git",
|
|
545
|
+
"dist",
|
|
546
|
+
"build",
|
|
547
|
+
".next",
|
|
548
|
+
".turbo",
|
|
549
|
+
"coverage",
|
|
550
|
+
".cache"
|
|
551
|
+
]);
|
|
552
|
+
var MAX_FILE_SIZE = 1024 * 1024;
|
|
553
|
+
function walkSourceTree(rootDir) {
|
|
554
|
+
const sources = /* @__PURE__ */ new Map();
|
|
555
|
+
function walk2(dir) {
|
|
556
|
+
let entries;
|
|
557
|
+
try {
|
|
558
|
+
entries = readdirSync2(dir);
|
|
559
|
+
} catch {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
for (const entry of entries) {
|
|
563
|
+
if (SKIP_DIRECTORIES.has(entry)) continue;
|
|
564
|
+
const fullPath = join3(dir, entry);
|
|
565
|
+
let s;
|
|
566
|
+
try {
|
|
567
|
+
s = statSync(fullPath);
|
|
568
|
+
} catch {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (s.isDirectory()) {
|
|
572
|
+
walk2(fullPath);
|
|
573
|
+
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
|
|
574
|
+
if (s.size > MAX_FILE_SIZE) continue;
|
|
575
|
+
try {
|
|
576
|
+
sources.set(fullPath, readFileSync3(fullPath, "utf8"));
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
walk2(rootDir);
|
|
583
|
+
return sources;
|
|
584
|
+
}
|
|
585
|
+
function collectDeclaredInteractions(projectRoot) {
|
|
586
|
+
const manifestPath = join3(projectRoot, ".decantr", "context", "pack-manifest.json");
|
|
587
|
+
if (!existsSync3(manifestPath)) return [];
|
|
588
|
+
let manifest;
|
|
589
|
+
try {
|
|
590
|
+
manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
591
|
+
} catch {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
const all = [];
|
|
595
|
+
const pages = manifest.pages ?? [];
|
|
596
|
+
const contextDir = join3(projectRoot, ".decantr", "context");
|
|
597
|
+
for (const page of pages) {
|
|
598
|
+
const packPath = join3(contextDir, page.json);
|
|
599
|
+
if (!existsSync3(packPath)) continue;
|
|
600
|
+
let pack;
|
|
601
|
+
try {
|
|
602
|
+
pack = JSON.parse(readFileSync3(packPath, "utf8"));
|
|
603
|
+
} catch {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const patterns = pack.data?.patterns ?? [];
|
|
607
|
+
for (const pat of patterns) {
|
|
608
|
+
if (Array.isArray(pat.interactions)) {
|
|
609
|
+
all.push(...pat.interactions);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return all;
|
|
614
|
+
}
|
|
615
|
+
function scanProjectInteractions(projectRoot) {
|
|
616
|
+
const declared = collectDeclaredInteractions(projectRoot);
|
|
617
|
+
if (declared.length === 0) return [];
|
|
618
|
+
const sources = walkSourceTree(projectRoot);
|
|
619
|
+
if (sources.size === 0) return [];
|
|
620
|
+
const missing = verifyInteractionsInSource(declared, sources);
|
|
621
|
+
return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
|
|
622
|
+
}
|
|
388
623
|
|
|
389
624
|
// src/analyzers/routes.ts
|
|
390
625
|
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
@@ -1158,7 +1393,9 @@ function readSmallText(projectRoot, relPath) {
|
|
|
1158
1393
|
}
|
|
1159
1394
|
}
|
|
1160
1395
|
function detectConflicts(projectRoot, items) {
|
|
1161
|
-
const text = items.filter(
|
|
1396
|
+
const text = items.filter(
|
|
1397
|
+
(item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)
|
|
1398
|
+
).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
|
|
1162
1399
|
const conflicts = [];
|
|
1163
1400
|
const frameworkSignals = [
|
|
1164
1401
|
["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
|
|
@@ -1172,9 +1409,7 @@ function detectConflicts(projectRoot, items) {
|
|
|
1172
1409
|
);
|
|
1173
1410
|
}
|
|
1174
1411
|
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
|
-
);
|
|
1412
|
+
const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(text);
|
|
1178
1413
|
if (forbidsTailwind && endorsesTailwind) {
|
|
1179
1414
|
conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
|
|
1180
1415
|
}
|
|
@@ -1190,9 +1425,9 @@ function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
|
1190
1425
|
try {
|
|
1191
1426
|
const essence = JSON.parse(content);
|
|
1192
1427
|
const risks = [];
|
|
1193
|
-
if (essence.version !== "
|
|
1428
|
+
if (essence.version !== "4.0.0") {
|
|
1194
1429
|
risks.push(
|
|
1195
|
-
`decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; migrate or review before treating it as current brownfield doctrine.`
|
|
1430
|
+
`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
1431
|
);
|
|
1197
1432
|
}
|
|
1198
1433
|
if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
|
|
@@ -1210,7 +1445,9 @@ function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
|
1210
1445
|
function detectStaleRisks(projectRoot, items) {
|
|
1211
1446
|
const pathRisks = items.filter(
|
|
1212
1447
|
(item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
|
|
1213
|
-
).slice(0, 12).map(
|
|
1448
|
+
).slice(0, 12).map(
|
|
1449
|
+
(item) => `${item.path} may be historical; verify before treating it as current doctrine.`
|
|
1450
|
+
);
|
|
1214
1451
|
return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
|
|
1215
1452
|
}
|
|
1216
1453
|
function scanAmbientContext(projectRoot) {
|
|
@@ -1402,17 +1639,23 @@ function readDoctrineMap(projectRoot) {
|
|
|
1402
1639
|
}
|
|
1403
1640
|
|
|
1404
1641
|
export {
|
|
1405
|
-
scanProjectInteractions,
|
|
1406
1642
|
scanRoutes,
|
|
1407
1643
|
scanStyling,
|
|
1408
1644
|
scanAmbientContext,
|
|
1409
1645
|
createDoctrineMap,
|
|
1410
1646
|
writeDoctrineMap,
|
|
1411
1647
|
readDoctrineMap,
|
|
1412
|
-
buildGuardRegistryContext,
|
|
1413
1648
|
sendGuardMetrics,
|
|
1414
1649
|
isOptedIn,
|
|
1415
1650
|
optIn,
|
|
1416
1651
|
sendCliCommandTelemetry,
|
|
1417
|
-
|
|
1652
|
+
sendProjectHealthReportTelemetry,
|
|
1653
|
+
sendNewProjectCompletedTelemetry,
|
|
1654
|
+
sendProjectHealthPromptTelemetry,
|
|
1655
|
+
sendProjectHealthCiFailedTelemetry,
|
|
1656
|
+
sendStudioStartedTelemetry,
|
|
1657
|
+
sendStudioHealthRefreshedTelemetry,
|
|
1658
|
+
collectMetrics,
|
|
1659
|
+
buildGuardRegistryContext,
|
|
1660
|
+
scanProjectInteractions
|
|
1418
1661
|
};
|