@distributionos/cli 0.1.15 → 0.1.16
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 +7 -7
- package/package.json +1 -1
- package/src/api.js +125 -125
- package/src/bootstrap.js +27 -25
- package/src/cli.js +216 -214
- package/src/constants.js +8 -8
- package/src/initialization.js +63 -63
- package/src/mcp-config.js +319 -187
- package/src/mutate.js +46 -43
- package/src/oauth.js +30 -30
- package/src/plan.js +175 -169
- package/src/setup-report.js +247 -216
- package/src/validation.js +108 -0
package/src/setup-report.js
CHANGED
|
@@ -1,222 +1,253 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
CLI_LOCAL_VERSION,
|
|
5
|
-
CURRENT_ANALYTICS_CONTRACT_VERSION,
|
|
6
|
-
CURRENT_BOOTSTRAP_VERSION,
|
|
7
|
-
CURRENT_SETUP_CONTRACT_VERSION,
|
|
8
|
-
MCP_SERVER_URL,
|
|
9
|
-
} from './constants.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
detectedCapabilities.add('agent.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
CLI_LOCAL_VERSION,
|
|
5
|
+
CURRENT_ANALYTICS_CONTRACT_VERSION,
|
|
6
|
+
CURRENT_BOOTSTRAP_VERSION,
|
|
7
|
+
CURRENT_SETUP_CONTRACT_VERSION,
|
|
8
|
+
MCP_SERVER_URL,
|
|
9
|
+
} from './constants.js';
|
|
10
|
+
import { inspectHighIntentInstrumentation } from './validation.js';
|
|
11
|
+
|
|
12
|
+
const BOOTSTRAP_BLOCK_RE =
|
|
13
|
+
/<!--\s*DISTRIBUTIONOS:START([^>]*)-->[\s\S]*?<!--\s*DISTRIBUTIONOS:END\s*-->/i;
|
|
14
|
+
const ANALYTICS_BLOCK_RE =
|
|
15
|
+
/DISTRIBUTIONOS:START analytics[\s\S]*?DISTRIBUTIONOS:END analytics/i;
|
|
16
|
+
|
|
17
|
+
export async function buildSetupReport(plan, phase = 'plan', details = {}) {
|
|
18
|
+
const detectedCapabilities = new Set([
|
|
19
|
+
'analytics.verificationReady',
|
|
20
|
+
'setup.reporting',
|
|
21
|
+
]);
|
|
22
|
+
const managedFiles = [];
|
|
23
|
+
let blockedReason = null;
|
|
24
|
+
|
|
25
|
+
const bootstrapVersion = detectBootstrapVersion(plan);
|
|
26
|
+
if (bootstrapVersion && plan.bootstrap.action === 'none') {
|
|
27
|
+
detectedCapabilities.add('agent.bootstrap');
|
|
28
|
+
detectedCapabilities.add('agent.liveInstructions');
|
|
29
|
+
}
|
|
30
|
+
managedFiles.push({
|
|
31
|
+
path: plan.bootstrap.target,
|
|
32
|
+
type: 'agent.bootstrap',
|
|
33
|
+
action: plan.bootstrap.action,
|
|
34
|
+
status: plan.bootstrap.action === 'none' ? 'current' : 'planned',
|
|
35
|
+
reason: plan.bootstrap.reason,
|
|
36
|
+
});
|
|
37
|
+
|
|
37
38
|
if (plan.mcp?.action === 'none') {
|
|
38
39
|
detectedCapabilities.add('agent.mcp.remoteConfig');
|
|
40
|
+
}
|
|
41
|
+
if (plan.codexMcp?.action === 'none') {
|
|
42
|
+
detectedCapabilities.add('agent.mcp.codexConfig');
|
|
43
|
+
}
|
|
44
|
+
if (plan.codexMcp?.action === 'manual') {
|
|
45
|
+
blockedReason = plan.codexMcp.reason;
|
|
39
46
|
} else if (plan.mcp?.action === 'manual') {
|
|
40
47
|
blockedReason = plan.mcp.reason;
|
|
41
48
|
}
|
|
42
|
-
if (plan.
|
|
49
|
+
if (plan.codexMcp) {
|
|
43
50
|
managedFiles.push({
|
|
44
|
-
path: plan.
|
|
45
|
-
type: 'agent.mcp.
|
|
46
|
-
action: plan.
|
|
47
|
-
status: plan.
|
|
48
|
-
reason: plan.
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
51
|
+
path: plan.codexMcp.path,
|
|
52
|
+
type: 'agent.mcp.codexConfig',
|
|
53
|
+
action: plan.codexMcp.action,
|
|
54
|
+
status: plan.codexMcp.action === 'none' ? 'current' : plan.codexMcp.action === 'manual' ? 'blocked_manual' : 'planned',
|
|
55
|
+
reason: plan.codexMcp.reason,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (plan.mcp) {
|
|
59
|
+
managedFiles.push({
|
|
60
|
+
path: plan.mcp.path,
|
|
61
|
+
type: 'agent.mcp.remoteConfig',
|
|
62
|
+
action: plan.mcp.action,
|
|
63
|
+
status: plan.mcp.action === 'none' ? 'current' : plan.mcp.action === 'manual' ? 'blocked_manual' : 'planned',
|
|
64
|
+
reason: plan.mcp.reason,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const analytics = await inspectAnalyticsInstall(plan);
|
|
69
|
+
if (analytics.publicTracker) detectedCapabilities.add('analytics.publicTracker');
|
|
70
|
+
if (analytics.routePrivacy) detectedCapabilities.add('analytics.routePrivacy');
|
|
71
|
+
const highIntent = await inspectHighIntentInstrumentation(plan.cwd, {
|
|
72
|
+
files: plan.repo.files,
|
|
73
|
+
contentFiles: plan.repo.contentFiles,
|
|
74
|
+
layoutTarget: plan.analytics.layoutTarget,
|
|
75
|
+
});
|
|
76
|
+
if (analytics.publicTracker && analytics.routePrivacy && highIntent.covered) {
|
|
77
|
+
detectedCapabilities.add('analytics.highIntentEvents');
|
|
78
|
+
}
|
|
79
|
+
if (plan.analytics.layoutTarget) {
|
|
80
|
+
managedFiles.push({
|
|
81
|
+
path: plan.analytics.layoutTarget,
|
|
82
|
+
type: 'analytics.publicTracker',
|
|
83
|
+
action: analytics.publicTracker && analytics.routePrivacy ? 'none' : 'update',
|
|
84
|
+
status: analytics.publicTracker && analytics.routePrivacy ? 'current' : 'planned',
|
|
85
|
+
reason: analytics.reason,
|
|
86
|
+
});
|
|
87
|
+
managedFiles.push({
|
|
88
|
+
path: plan.analytics.layoutTarget,
|
|
89
|
+
type: 'analytics.highIntentEvents',
|
|
90
|
+
action: analytics.publicTracker && analytics.routePrivacy && highIntent.covered ? 'none' : 'update',
|
|
91
|
+
status: analytics.publicTracker && analytics.routePrivacy && highIntent.covered ? 'current' : 'planned',
|
|
92
|
+
reason: highIntent.message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (plan.remote?.tokenName && /oauth/i.test(plan.remote.tokenName)) {
|
|
97
|
+
detectedCapabilities.add('agent.oauthReady');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const applyResult = details.applyResult ?? buildApplyResult(details);
|
|
101
|
+
return {
|
|
102
|
+
phase,
|
|
103
|
+
cliVersion: CLI_LOCAL_VERSION,
|
|
104
|
+
localContractVersion: CURRENT_SETUP_CONTRACT_VERSION,
|
|
105
|
+
detectedCapabilities: [...detectedCapabilities].sort(),
|
|
106
|
+
...(blockedReason ? { blockedReason } : {}),
|
|
107
|
+
requiresAgentRestart: Boolean(details.requiresAgentRestart),
|
|
108
|
+
managedFiles: managedFiles.map(sanitizeManagedFile),
|
|
109
|
+
repo: {
|
|
110
|
+
framework: plan.repo.framework,
|
|
111
|
+
packageManager: plan.repo.packageManager,
|
|
112
|
+
layoutTarget: plan.analytics.layoutTarget ?? null,
|
|
113
|
+
routeCounts: {
|
|
114
|
+
allowed: plan.repo.routePrivacy.allowed.length,
|
|
115
|
+
blocked: plan.repo.routePrivacy.blocked.length,
|
|
116
|
+
review: plan.repo.routePrivacy.review.length,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
...(applyResult ? { applyResult } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function formatSetupReportSummary(setupReport) {
|
|
124
|
+
if (!setupReport) return [];
|
|
125
|
+
const localSetup = setupReport.result?.localSetup ?? null;
|
|
126
|
+
const lines = [
|
|
127
|
+
'Local setup contract',
|
|
128
|
+
`- CLI version: ${CLI_LOCAL_VERSION}`,
|
|
129
|
+
`- Local contract: ${CURRENT_SETUP_CONTRACT_VERSION}`,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
if (setupReport.status === 'skipped') {
|
|
133
|
+
lines.push('- Server report: skipped by --skip-setup-report');
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (setupReport.status === 'failed') {
|
|
138
|
+
lines.push(`- Server report: failed (${setupReport.error})`);
|
|
139
|
+
lines.push('- Local file changes are still reviewable; DistributionOS may not show the latest install state until reporting succeeds.');
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!localSetup) {
|
|
144
|
+
lines.push('- Server report: pending');
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lines.push(`- Server status: ${localSetup.status}`);
|
|
149
|
+
lines.push(`- Missing capabilities: ${formatList(localSetup.missingCapabilities)}`);
|
|
150
|
+
if (localSetup.status !== 'current') {
|
|
151
|
+
lines.push(`- Review command: ${localSetup.updateCommand}`);
|
|
152
|
+
lines.push(`- Apply command: ${localSetup.applyCommand}`);
|
|
153
|
+
}
|
|
154
|
+
if (localSetup.requiresAgentRestart) {
|
|
155
|
+
lines.push('- Agent restart: required after apply so MCP changes load.');
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function detectBootstrapVersion(plan) {
|
|
161
|
+
const current = (plan.repo.instructionFiles ?? []).find((file) => file.hasManagedDistributionOsBlock);
|
|
162
|
+
return current?.managedDistributionOsVersion ??
|
|
163
|
+
(plan.bootstrap.action === 'none' ? CURRENT_BOOTSTRAP_VERSION : null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function inspectAnalyticsInstall(plan) {
|
|
167
|
+
const target = plan.analytics.layoutTarget;
|
|
168
|
+
if (!target) {
|
|
169
|
+
return {
|
|
170
|
+
publicTracker: false,
|
|
171
|
+
routePrivacy: false,
|
|
172
|
+
reason: 'No supported shared analytics layout was detected.',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let content = '';
|
|
177
|
+
try {
|
|
178
|
+
content = await fs.readFile(path.join(plan.cwd, target), 'utf8');
|
|
179
|
+
} catch {
|
|
180
|
+
return {
|
|
181
|
+
publicTracker: false,
|
|
182
|
+
routePrivacy: false,
|
|
183
|
+
reason: 'Analytics layout could not be read.',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const managed = ANALYTICS_BLOCK_RE.test(content);
|
|
188
|
+
const publicTracker = managed && content.includes(`${new URL(MCP_SERVER_URL).origin}/api/analytics/script/`);
|
|
189
|
+
const contractVersion = plan.analytics.contractVersion || CURRENT_ANALYTICS_CONTRACT_VERSION;
|
|
190
|
+
const routePrivacy =
|
|
191
|
+
managed &&
|
|
192
|
+
content.includes('distributionOSAnalyticsConfig') &&
|
|
193
|
+
content.includes(contractVersion) &&
|
|
194
|
+
content.includes('/workflow/**') &&
|
|
195
|
+
content.includes('/workflows/**');
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
publicTracker,
|
|
199
|
+
routePrivacy,
|
|
200
|
+
reason: publicTracker && routePrivacy
|
|
201
|
+
? 'Managed analytics install is current.'
|
|
202
|
+
: 'Managed analytics install is missing or stale.',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildApplyResult(details) {
|
|
207
|
+
if (!details.changes && !details.validation && !details.inspection) return null;
|
|
208
|
+
return {
|
|
209
|
+
changedFiles: [...new Set((details.changes ?? [])
|
|
210
|
+
.filter((change) => change.changed && change.file)
|
|
211
|
+
.map((change) => change.file))],
|
|
212
|
+
validation: (details.validation ?? []).map((result) => ({
|
|
213
|
+
name: result.name,
|
|
214
|
+
status: result.exitCode === 0
|
|
215
|
+
? 'passed'
|
|
216
|
+
: result.introducedFailure
|
|
217
|
+
? 'failed_after_cli_changes'
|
|
218
|
+
: result.preExistingFailure
|
|
219
|
+
? 'pre_existing_failure'
|
|
220
|
+
: 'failed',
|
|
221
|
+
})),
|
|
222
|
+
inspection: (details.inspection ?? []).map((result) => ({
|
|
223
|
+
name: result.name,
|
|
224
|
+
status: result.status,
|
|
225
|
+
})),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sanitizeManagedFile(file) {
|
|
230
|
+
return {
|
|
231
|
+
path: file.path,
|
|
232
|
+
type: file.type,
|
|
233
|
+
action: file.action,
|
|
234
|
+
status: file.status,
|
|
235
|
+
reason: file.reason,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatList(values) {
|
|
240
|
+
if (!Array.isArray(values) || values.length === 0) return 'none';
|
|
241
|
+
return values.join(', ');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function extractBootstrapMetadata(content) {
|
|
245
|
+
const match = String(content ?? '').match(BOOTSTRAP_BLOCK_RE);
|
|
246
|
+
if (!match) return { hasManagedDistributionOsBlock: false, version: null, appId: null };
|
|
247
|
+
const attrs = match[1] ?? '';
|
|
248
|
+
return {
|
|
249
|
+
hasManagedDistributionOsBlock: true,
|
|
250
|
+
version: attrs.match(/\bversion=([^\s>]+)/i)?.[1] ?? null,
|
|
251
|
+
appId: attrs.match(/\bappId=([^\s>]+)/i)?.[1] ?? null,
|
|
252
|
+
};
|
|
253
|
+
}
|
package/src/validation.js
CHANGED
|
@@ -106,9 +106,78 @@ export async function inspectInstallArtifacts(plan) {
|
|
|
106
106
|
const markerInspection = await inspectMarkers(plan.cwd, plan.repo.contentFiles);
|
|
107
107
|
results.push(markerInspection.content);
|
|
108
108
|
results.push(markerInspection.cta);
|
|
109
|
+
results.push(await inspectHighIntentInstrumentation(plan.cwd, {
|
|
110
|
+
files: plan.repo.files,
|
|
111
|
+
contentFiles: plan.repo.contentFiles,
|
|
112
|
+
layoutTarget: plan.analytics.layoutTarget,
|
|
113
|
+
}));
|
|
109
114
|
return results;
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
export async function inspectHighIntentInstrumentation(cwd, input = {}) {
|
|
118
|
+
const files = highIntentCandidateFiles(input);
|
|
119
|
+
let scanned = 0;
|
|
120
|
+
let ctaMarkerCount = 0;
|
|
121
|
+
let checkoutStartCount = 0;
|
|
122
|
+
let successEventCount = 0;
|
|
123
|
+
const highIntentSurfaceFiles = new Set();
|
|
124
|
+
const checkoutSurfaceFiles = new Set();
|
|
125
|
+
const successSurfaceFiles = new Set();
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const text = await fs.readFile(safeResolve(cwd, file), 'utf8').catch(() => '');
|
|
129
|
+
if (!text) continue;
|
|
130
|
+
scanned += 1;
|
|
131
|
+
const haystack = `${file}\n${text}`;
|
|
132
|
+
const lower = haystack.toLowerCase();
|
|
133
|
+
if (HIGH_INTENT_SURFACE_RE.test(lower)) highIntentSurfaceFiles.add(file);
|
|
134
|
+
if (CHECKOUT_SURFACE_RE.test(lower)) checkoutSurfaceFiles.add(file);
|
|
135
|
+
if (SUCCESS_SURFACE_RE.test(lower)) successSurfaceFiles.add(file);
|
|
136
|
+
ctaMarkerCount += countEventHookOccurrences(text, 'cta_click');
|
|
137
|
+
ctaMarkerCount += countOccurrences(text, 'data-dos-link-id');
|
|
138
|
+
checkoutStartCount += countEventHookOccurrences(text, 'checkout_started');
|
|
139
|
+
for (const eventName of SUCCESS_EVENT_NAMES) {
|
|
140
|
+
successEventCount += countEventHookOccurrences(text, eventName);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const hasObviousHighIntentSurface = highIntentSurfaceFiles.size > 0;
|
|
145
|
+
const hasCheckoutSurface = checkoutSurfaceFiles.size > 0;
|
|
146
|
+
const hasSuccessSurface = successSurfaceFiles.size > 0;
|
|
147
|
+
const hasCoverage = ctaMarkerCount > 0 || checkoutStartCount > 0 || successEventCount > 0;
|
|
148
|
+
const checkoutCovered = !hasCheckoutSurface || checkoutStartCount > 0;
|
|
149
|
+
const successCovered = !hasSuccessSurface || successEventCount > 0;
|
|
150
|
+
const covered = !hasObviousHighIntentSurface || (hasCoverage && checkoutCovered && successCovered);
|
|
151
|
+
|
|
152
|
+
const missing = [];
|
|
153
|
+
if (hasObviousHighIntentSurface && !hasCoverage) {
|
|
154
|
+
missing.push('no CTA markers or explicit high-intent events found');
|
|
155
|
+
}
|
|
156
|
+
if (hasCheckoutSurface && checkoutStartCount === 0) {
|
|
157
|
+
missing.push('checkout/payment-start surface found without checkout_started');
|
|
158
|
+
}
|
|
159
|
+
if (hasSuccessSurface && successEventCount === 0) {
|
|
160
|
+
missing.push('signup/waitlist/email surface found without success event');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const evidence = [
|
|
164
|
+
`${scanned} file${scanned === 1 ? '' : 's'} scanned`,
|
|
165
|
+
`${highIntentSurfaceFiles.size} high-intent surface${highIntentSurfaceFiles.size === 1 ? '' : 's'}`,
|
|
166
|
+
`${ctaMarkerCount} CTA marker${ctaMarkerCount === 1 ? '' : 's'}`,
|
|
167
|
+
`${checkoutStartCount} checkout_started hook${checkoutStartCount === 1 ? '' : 's'}`,
|
|
168
|
+
`${successEventCount} success-event hook${successEventCount === 1 ? '' : 's'}`,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
name: 'high-intent events',
|
|
173
|
+
status: covered ? 'passed' : 'warning',
|
|
174
|
+
covered,
|
|
175
|
+
message: covered
|
|
176
|
+
? `High-intent analytics coverage looks current (${evidence.join(', ')}).`
|
|
177
|
+
: `High-intent analytics needs review: ${missing.join('; ')} (${evidence.join(', ')}).`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
112
181
|
async function runCommand({ name, command, cwd, timeoutMs }) {
|
|
113
182
|
try {
|
|
114
183
|
const { stdout, stderr } = await execAsync(command, {
|
|
@@ -238,6 +307,45 @@ function countOccurrences(value, needle) {
|
|
|
238
307
|
return String(value).split(needle).length - 1;
|
|
239
308
|
}
|
|
240
309
|
|
|
310
|
+
function countEventHookOccurrences(value, eventName) {
|
|
311
|
+
const escapedEvent = escapeRegExp(eventName);
|
|
312
|
+
const patterns = [
|
|
313
|
+
new RegExp(`data-dos-event=["']${escapedEvent}["']`, 'g'),
|
|
314
|
+
new RegExp(`\\.track\\(\\s*["']${escapedEvent}["']`, 'g'),
|
|
315
|
+
];
|
|
316
|
+
return patterns.reduce((total, pattern) => total + (String(value).match(pattern)?.length ?? 0), 0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function escapeRegExp(value) {
|
|
320
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const HIGH_INTENT_SOURCE_RE = /\.(jsx?|tsx?|mdx?|html)$/i;
|
|
324
|
+
const PRIVATE_PATH_RE = /(^|\/)(dashboard|account|billing|settings|auth|login|admin|api|product|products|project|projects|workspace|workspaces|workflow|workflows|customer|customers|client|clients|user|users|portal)(\/|$)/i;
|
|
325
|
+
const HIGH_INTENT_PATH_RE = /(^|\/)(pricing|price|plans?|subscribe|subscription|checkout|payment|payments|trial|waitlist|newsletter|contact|landing|home|page)\b/i;
|
|
326
|
+
const HIGH_INTENT_SURFACE_RE = /\b(pricing|price|plan|subscribe|subscription|checkout|payment|trial|buy|purchase|waitlist|newsletter|email capture|email signup|sign up|signup)\b/i;
|
|
327
|
+
const CHECKOUT_SURFACE_RE = /\b(checkout|payment|subscribe|subscription|trial|buy|purchase|pricing|price|plan)\b/i;
|
|
328
|
+
const SUCCESS_SURFACE_RE = /\b(waitlist|newsletter|email capture|email signup|sign up|signup)\b/i;
|
|
329
|
+
const SUCCESS_EVENT_NAMES = ['signup', 'waitlist_joined', 'email_subscribed'];
|
|
330
|
+
|
|
331
|
+
function highIntentCandidateFiles(input) {
|
|
332
|
+
const values = [
|
|
333
|
+
...(Array.isArray(input.files) ? input.files : []),
|
|
334
|
+
...(Array.isArray(input.contentFiles) ? input.contentFiles : []),
|
|
335
|
+
input.layoutTarget,
|
|
336
|
+
].filter((value) => typeof value === 'string' && value.length > 0);
|
|
337
|
+
|
|
338
|
+
return [...new Set(values)]
|
|
339
|
+
.filter((file) => HIGH_INTENT_SOURCE_RE.test(file))
|
|
340
|
+
.filter((file) => !PRIVATE_PATH_RE.test(file))
|
|
341
|
+
.filter((file) => (
|
|
342
|
+
HIGH_INTENT_PATH_RE.test(file)
|
|
343
|
+
|| (Array.isArray(input.contentFiles) && input.contentFiles.includes(file))
|
|
344
|
+
|| file === input.layoutTarget
|
|
345
|
+
))
|
|
346
|
+
.slice(0, 200);
|
|
347
|
+
}
|
|
348
|
+
|
|
241
349
|
function scriptSrcFromTag(scriptTag) {
|
|
242
350
|
if (!scriptTag) return null;
|
|
243
351
|
const match = String(scriptTag).match(/\ssrc=["']([^"']+)["']/i);
|