@distributionos/cli 0.1.14 → 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.
@@ -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
- const BOOTSTRAP_BLOCK_RE =
12
- /<!--\s*DISTRIBUTIONOS:START([^>]*)-->[\s\S]*?<!--\s*DISTRIBUTIONOS:END\s*-->/i;
13
- const ANALYTICS_BLOCK_RE =
14
- /DISTRIBUTIONOS:START analytics[\s\S]*?DISTRIBUTIONOS:END analytics/i;
15
-
16
- export async function buildSetupReport(plan, phase = 'plan', details = {}) {
17
- const detectedCapabilities = new Set([
18
- 'analytics.verificationReady',
19
- 'setup.reporting',
20
- ]);
21
- const managedFiles = [];
22
- let blockedReason = null;
23
-
24
- const bootstrapVersion = detectBootstrapVersion(plan);
25
- if (bootstrapVersion && plan.bootstrap.action === 'none') {
26
- detectedCapabilities.add('agent.bootstrap');
27
- detectedCapabilities.add('agent.liveInstructions');
28
- }
29
- managedFiles.push({
30
- path: plan.bootstrap.target,
31
- type: 'agent.bootstrap',
32
- action: plan.bootstrap.action,
33
- status: plan.bootstrap.action === 'none' ? 'current' : 'planned',
34
- reason: plan.bootstrap.reason,
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.mcp) {
49
+ if (plan.codexMcp) {
43
50
  managedFiles.push({
44
- path: plan.mcp.path,
45
- type: 'agent.mcp.remoteConfig',
46
- action: plan.mcp.action,
47
- status: plan.mcp.action === 'none' ? 'current' : plan.mcp.action === 'manual' ? 'blocked_manual' : 'planned',
48
- reason: plan.mcp.reason,
49
- });
50
- }
51
-
52
- const analytics = await inspectAnalyticsInstall(plan);
53
- if (analytics.publicTracker) detectedCapabilities.add('analytics.publicTracker');
54
- if (analytics.routePrivacy) detectedCapabilities.add('analytics.routePrivacy');
55
- if (plan.analytics.layoutTarget) {
56
- managedFiles.push({
57
- path: plan.analytics.layoutTarget,
58
- type: 'analytics.publicTracker',
59
- action: analytics.publicTracker && analytics.routePrivacy ? 'none' : 'update',
60
- status: analytics.publicTracker && analytics.routePrivacy ? 'current' : 'planned',
61
- reason: analytics.reason,
62
- });
63
- }
64
-
65
- if (plan.remote?.tokenName && /oauth/i.test(plan.remote.tokenName)) {
66
- detectedCapabilities.add('agent.oauthReady');
67
- }
68
-
69
- const applyResult = details.applyResult ?? buildApplyResult(details);
70
- return {
71
- phase,
72
- cliVersion: CLI_LOCAL_VERSION,
73
- localContractVersion: CURRENT_SETUP_CONTRACT_VERSION,
74
- detectedCapabilities: [...detectedCapabilities].sort(),
75
- ...(blockedReason ? { blockedReason } : {}),
76
- requiresAgentRestart: Boolean(details.requiresAgentRestart),
77
- managedFiles: managedFiles.map(sanitizeManagedFile),
78
- repo: {
79
- framework: plan.repo.framework,
80
- packageManager: plan.repo.packageManager,
81
- layoutTarget: plan.analytics.layoutTarget ?? null,
82
- routeCounts: {
83
- allowed: plan.repo.routePrivacy.allowed.length,
84
- blocked: plan.repo.routePrivacy.blocked.length,
85
- review: plan.repo.routePrivacy.review.length,
86
- },
87
- },
88
- ...(applyResult ? { applyResult } : {}),
89
- };
90
- }
91
-
92
- export function formatSetupReportSummary(setupReport) {
93
- if (!setupReport) return [];
94
- const localSetup = setupReport.result?.localSetup ?? null;
95
- const lines = [
96
- 'Local setup contract',
97
- `- CLI version: ${CLI_LOCAL_VERSION}`,
98
- `- Local contract: ${CURRENT_SETUP_CONTRACT_VERSION}`,
99
- ];
100
-
101
- if (setupReport.status === 'skipped') {
102
- lines.push('- Server report: skipped by --skip-setup-report');
103
- return lines;
104
- }
105
-
106
- if (setupReport.status === 'failed') {
107
- lines.push(`- Server report: failed (${setupReport.error})`);
108
- lines.push('- Local file changes are still reviewable; DistributionOS may not show the latest install state until reporting succeeds.');
109
- return lines;
110
- }
111
-
112
- if (!localSetup) {
113
- lines.push('- Server report: pending');
114
- return lines;
115
- }
116
-
117
- lines.push(`- Server status: ${localSetup.status}`);
118
- lines.push(`- Missing capabilities: ${formatList(localSetup.missingCapabilities)}`);
119
- if (localSetup.status !== 'current') {
120
- lines.push(`- Review command: ${localSetup.updateCommand}`);
121
- lines.push(`- Apply command: ${localSetup.applyCommand}`);
122
- }
123
- if (localSetup.requiresAgentRestart) {
124
- lines.push('- Agent restart: required after apply so MCP changes load.');
125
- }
126
- return lines;
127
- }
128
-
129
- function detectBootstrapVersion(plan) {
130
- const current = (plan.repo.instructionFiles ?? []).find((file) => file.hasManagedDistributionOsBlock);
131
- return current?.managedDistributionOsVersion ??
132
- (plan.bootstrap.action === 'none' ? CURRENT_BOOTSTRAP_VERSION : null);
133
- }
134
-
135
- async function inspectAnalyticsInstall(plan) {
136
- const target = plan.analytics.layoutTarget;
137
- if (!target) {
138
- return {
139
- publicTracker: false,
140
- routePrivacy: false,
141
- reason: 'No supported shared analytics layout was detected.',
142
- };
143
- }
144
-
145
- let content = '';
146
- try {
147
- content = await fs.readFile(path.join(plan.cwd, target), 'utf8');
148
- } catch {
149
- return {
150
- publicTracker: false,
151
- routePrivacy: false,
152
- reason: 'Analytics layout could not be read.',
153
- };
154
- }
155
-
156
- const managed = ANALYTICS_BLOCK_RE.test(content);
157
- const publicTracker = managed && content.includes(`${new URL(MCP_SERVER_URL).origin}/api/analytics/script/`);
158
- const contractVersion = plan.analytics.contractVersion || CURRENT_ANALYTICS_CONTRACT_VERSION;
159
- const routePrivacy =
160
- managed &&
161
- content.includes('distributionOSAnalyticsConfig') &&
162
- content.includes(contractVersion) &&
163
- content.includes('/workflow/**') &&
164
- content.includes('/workflows/**');
165
-
166
- return {
167
- publicTracker,
168
- routePrivacy,
169
- reason: publicTracker && routePrivacy
170
- ? 'Managed analytics install is current.'
171
- : 'Managed analytics install is missing or stale.',
172
- };
173
- }
174
-
175
- function buildApplyResult(details) {
176
- if (!details.changes && !details.validation && !details.inspection) return null;
177
- return {
178
- changedFiles: [...new Set((details.changes ?? [])
179
- .filter((change) => change.changed && change.file)
180
- .map((change) => change.file))],
181
- validation: (details.validation ?? []).map((result) => ({
182
- name: result.name,
183
- status: result.exitCode === 0
184
- ? 'passed'
185
- : result.introducedFailure
186
- ? 'failed_after_cli_changes'
187
- : result.preExistingFailure
188
- ? 'pre_existing_failure'
189
- : 'failed',
190
- })),
191
- inspection: (details.inspection ?? []).map((result) => ({
192
- name: result.name,
193
- status: result.status,
194
- })),
195
- };
196
- }
197
-
198
- function sanitizeManagedFile(file) {
199
- return {
200
- path: file.path,
201
- type: file.type,
202
- action: file.action,
203
- status: file.status,
204
- reason: file.reason,
205
- };
206
- }
207
-
208
- function formatList(values) {
209
- if (!Array.isArray(values) || values.length === 0) return 'none';
210
- return values.join(', ');
211
- }
212
-
213
- export function extractBootstrapMetadata(content) {
214
- const match = String(content ?? '').match(BOOTSTRAP_BLOCK_RE);
215
- if (!match) return { hasManagedDistributionOsBlock: false, version: null, appId: null };
216
- const attrs = match[1] ?? '';
217
- return {
218
- hasManagedDistributionOsBlock: true,
219
- version: attrs.match(/\bversion=([^\s>]+)/i)?.[1] ?? null,
220
- appId: attrs.match(/\bappId=([^\s>]+)/i)?.[1] ?? null,
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);