@contentful/experience-design-system-cli 2.24.1-dev-build-1fdd4a0.0 → 2.24.1-dev-build-1db4d13.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 +26 -0
- package/dist/package.json +2 -1
- package/dist/src/analytics/apply.d.ts +6 -0
- package/dist/src/analytics/apply.js +30 -0
- package/dist/src/analytics/client.d.ts +8 -0
- package/dist/src/analytics/client.js +76 -0
- package/dist/src/analytics/constants.d.ts +4 -0
- package/dist/src/analytics/constants.js +4 -0
- package/dist/src/analytics/env.d.ts +4 -0
- package/dist/src/analytics/env.js +14 -0
- package/dist/src/analytics/exit.d.ts +5 -0
- package/dist/src/analytics/exit.js +24 -0
- package/dist/src/analytics/index.d.ts +10 -0
- package/dist/src/analytics/index.js +9 -0
- package/dist/src/analytics/normalize.d.ts +3 -0
- package/dist/src/analytics/normalize.js +18 -0
- package/dist/src/analytics/os.d.ts +2 -0
- package/dist/src/analytics/os.js +13 -0
- package/dist/src/analytics/session.d.ts +3 -0
- package/dist/src/analytics/session.js +15 -0
- package/dist/src/analytics/tracker.d.ts +17 -0
- package/dist/src/analytics/tracker.js +126 -0
- package/dist/src/analytics/types.d.ts +28 -0
- package/dist/src/analytics/types.js +1 -0
- package/dist/src/analyze/command.js +8 -2
- package/dist/src/analyze/select/command.js +13 -9
- package/dist/src/analyze/select-agent/command.js +14 -11
- package/dist/src/apply/api-client.d.ts +3 -0
- package/dist/src/apply/api-client.js +14 -1
- package/dist/src/apply/command.d.ts +1 -1
- package/dist/src/apply/command.js +71 -50
- package/dist/src/credentials-store.d.ts +2 -0
- package/dist/src/credentials-store.js +3 -1
- package/dist/src/generate/command.js +15 -9
- package/dist/src/generate/edit/command.js +1 -0
- package/dist/src/import/orchestrator.js +18 -12
- package/dist/src/index.js +6 -1
- package/dist/src/print/command.js +16 -13
- package/dist/src/program.js +11 -1
- package/dist/src/setup/analytics-prompt.d.ts +13 -0
- package/dist/src/setup/analytics-prompt.js +24 -0
- package/dist/src/setup/command.js +16 -0
- package/package.json +6 -5
|
@@ -14,6 +14,7 @@ import { hashPromptForSkill } from '../session/cache-keys.js';
|
|
|
14
14
|
import { getRefineArtifactsRoot, getRefineSessionPaths } from '../analyze/select/persistence.js';
|
|
15
15
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
16
16
|
import { addAgentModelOptions } from '../lib/agent-model-options.js';
|
|
17
|
+
import { bindAnalyticsSessionId, exitWithAnalytics } from '../analytics/index.js';
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
18
19
|
const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
|
|
19
20
|
const DEFAULT_COMPONENT_CONCURRENCY = 10;
|
|
@@ -23,7 +24,8 @@ const invoker = createLocalCliAgentInvoker({
|
|
|
23
24
|
});
|
|
24
25
|
function die(message) {
|
|
25
26
|
process.stderr.write(`${message}\n`);
|
|
26
|
-
|
|
27
|
+
void exitWithAnalytics(1);
|
|
28
|
+
throw new Error('exit');
|
|
27
29
|
}
|
|
28
30
|
async function pathExists(p) {
|
|
29
31
|
return access(p)
|
|
@@ -276,7 +278,8 @@ function resolveSessionId(sessionFlag) {
|
|
|
276
278
|
.get();
|
|
277
279
|
if (!row) {
|
|
278
280
|
process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
|
|
279
|
-
|
|
281
|
+
void exitWithAnalytics(1);
|
|
282
|
+
throw new Error('exit');
|
|
280
283
|
}
|
|
281
284
|
return row.id;
|
|
282
285
|
}
|
|
@@ -340,6 +343,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
340
343
|
let allComponents;
|
|
341
344
|
if (skill === 'components') {
|
|
342
345
|
sessionId = resolveSessionId(opts.session);
|
|
346
|
+
await bindAnalyticsSessionId(sessionId);
|
|
343
347
|
const acceptedNames = await loadAcceptedNames(sessionId);
|
|
344
348
|
const db = openPipelineDb();
|
|
345
349
|
try {
|
|
@@ -396,7 +400,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
396
400
|
skillPathOverride: generatePromptPath,
|
|
397
401
|
});
|
|
398
402
|
process.stdout.write(prompt + '\n');
|
|
399
|
-
|
|
403
|
+
await exitWithAnalytics(0);
|
|
400
404
|
}
|
|
401
405
|
const binary = resolveBinary(agent);
|
|
402
406
|
if (!(await assertBinaryInPath(binary))) {
|
|
@@ -405,7 +409,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
405
409
|
skill,
|
|
406
410
|
sessionId: sessionId ?? '',
|
|
407
411
|
});
|
|
408
|
-
|
|
412
|
+
await exitWithAnalytics(1);
|
|
409
413
|
}
|
|
410
414
|
if (skill === 'components' && allComponents && sessionId) {
|
|
411
415
|
const db = openPipelineDb();
|
|
@@ -471,6 +475,8 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
471
475
|
resolvedSessionId = newId;
|
|
472
476
|
}
|
|
473
477
|
}
|
|
478
|
+
sessionId = resolvedSessionId;
|
|
479
|
+
await bindAnalyticsSessionId(resolvedSessionId);
|
|
474
480
|
const tokenPromptHash = await hashPromptForSkill('tokens');
|
|
475
481
|
// Check cache before invoking agent
|
|
476
482
|
if (!noCache) {
|
|
@@ -483,12 +489,12 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
483
489
|
// Skip agent invocation — jump to view
|
|
484
490
|
const viewResult = { skill, agent, sessionId: sessionId ?? '' };
|
|
485
491
|
if (process.stdout.isTTY) {
|
|
486
|
-
const { waitUntilExit } = render(createElement(GenerateView, { result: viewResult, onExit: () =>
|
|
492
|
+
const { waitUntilExit } = render(createElement(GenerateView, { result: viewResult, onExit: () => void exitWithAnalytics(0) }));
|
|
487
493
|
await waitUntilExit();
|
|
488
494
|
}
|
|
489
495
|
else {
|
|
490
496
|
process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession=${sessionId ?? ''}\n`);
|
|
491
|
-
|
|
497
|
+
await exitWithAnalytics(0);
|
|
492
498
|
}
|
|
493
499
|
return;
|
|
494
500
|
}
|
|
@@ -523,7 +529,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
523
529
|
process.stderr.write(`Error: agent produced no set_token calls.\n` +
|
|
524
530
|
`Run with --dry-run to inspect the prompt.\n\n` +
|
|
525
531
|
`Agent output:\n${result.stdout}\n`);
|
|
526
|
-
|
|
532
|
+
await exitWithAnalytics(1);
|
|
527
533
|
}
|
|
528
534
|
if (tokenWarnings.length > 0) {
|
|
529
535
|
process.stderr.write(`Warnings:\n${tokenWarnings.map((w) => ` ${w}`).join('\n')}\n`);
|
|
@@ -548,13 +554,13 @@ async function runGenerateSkill(skill, opts, verbose = false) {
|
|
|
548
554
|
if (process.stdout.isTTY) {
|
|
549
555
|
const { waitUntilExit } = render(createElement(GenerateView, {
|
|
550
556
|
result: viewResult,
|
|
551
|
-
onExit: () =>
|
|
557
|
+
onExit: () => void exitWithAnalytics(0),
|
|
552
558
|
}));
|
|
553
559
|
await waitUntilExit();
|
|
554
560
|
}
|
|
555
561
|
else {
|
|
556
562
|
process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession=${sessionId ?? ''}\n`);
|
|
557
|
-
|
|
563
|
+
await exitWithAnalytics(0);
|
|
558
564
|
}
|
|
559
565
|
}
|
|
560
566
|
function addAgentFlags(cmd) {
|
|
@@ -99,6 +99,7 @@ async function runNonInteractive(opts, skill) {
|
|
|
99
99
|
process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
|
|
100
100
|
}
|
|
101
101
|
export function registerGenerateEditCommand(parent, skill) {
|
|
102
|
+
// TODO(analytics): bindAnalyticsSessionId when generate edit ships — tracked in schema as generate_edit.
|
|
102
103
|
parent
|
|
103
104
|
.command('edit')
|
|
104
105
|
.description(`Review and correct generate ${skill} output before pushing`)
|
|
@@ -6,17 +6,19 @@ import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestS
|
|
|
6
6
|
import { detectSlotCycles, formatSlotCycleReport } from '../apply/command.js';
|
|
7
7
|
import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
|
|
8
8
|
import { buildPostPushUrl } from '../lib/contentful-urls.js';
|
|
9
|
-
import { getDebugLogger
|
|
9
|
+
import { getDebugLogger } from '../lib/debug-logger.js';
|
|
10
|
+
import { bindAnalyticsSession, emitSessionStarted } from '../analytics/index.js';
|
|
11
|
+
import { pipelineSubprocessEnv } from '../analytics/env.js';
|
|
10
12
|
function findCliPath() {
|
|
11
13
|
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
|
|
12
14
|
}
|
|
13
|
-
async function runStep(args, cliPath, env = {}, streamStderr = false) {
|
|
15
|
+
async function runStep(args, cliPath, analyticsSessionId, env = {}, streamStderr = false) {
|
|
14
16
|
const debug = getDebugLogger();
|
|
15
17
|
const startedAt = Date.now();
|
|
16
18
|
debug.event('import', 'subprocess.spawn', { cliPath, args });
|
|
17
19
|
return new Promise((res) => {
|
|
18
20
|
const child = execFile('node', [cliPath, ...args], {
|
|
19
|
-
env:
|
|
21
|
+
env: pipelineSubprocessEnv({ ...process.env, ...env }, analyticsSessionId),
|
|
20
22
|
});
|
|
21
23
|
let stdout = '';
|
|
22
24
|
let stderr = '';
|
|
@@ -108,6 +110,10 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
108
110
|
inputPath: projectRoot,
|
|
109
111
|
outDir,
|
|
110
112
|
});
|
|
113
|
+
await bindAnalyticsSession(sessionId, {
|
|
114
|
+
...(opts.spaceId ? { space_key: opts.spaceId, environment_key: opts.environmentId } : {}),
|
|
115
|
+
});
|
|
116
|
+
await emitSessionStarted('import');
|
|
111
117
|
progressWriter(`Experience Design System CLI — Pipeline Import`);
|
|
112
118
|
progressWriter(`Project: ${projectRoot}`);
|
|
113
119
|
progressWriter(`Output: ${outDir}`);
|
|
@@ -159,7 +165,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
159
165
|
if (opts.agent)
|
|
160
166
|
analyzeArgs.push('--agent', opts.agent);
|
|
161
167
|
}
|
|
162
|
-
const r = await runStep(analyzeArgs, cliPath);
|
|
168
|
+
const r = await runStep(analyzeArgs, cliPath, sessionId);
|
|
163
169
|
const durationMs = Date.now() - t0;
|
|
164
170
|
if (r.exitCode !== 0) {
|
|
165
171
|
if (r.stderr)
|
|
@@ -231,7 +237,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
231
237
|
editArgs.push('--select-all');
|
|
232
238
|
}
|
|
233
239
|
}
|
|
234
|
-
const rEdit = await runStep(editArgs, cliPath, { FORCE_COLOR: '1' }, useAgentSelect);
|
|
240
|
+
const rEdit = await runStep(editArgs, cliPath, sessionId, { FORCE_COLOR: '1' }, useAgentSelect);
|
|
235
241
|
const editDurationMs = Date.now() - t0Edit;
|
|
236
242
|
if (rEdit.exitCode !== 0) {
|
|
237
243
|
if (rEdit.stderr && !useAgentSelect)
|
|
@@ -292,7 +298,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
292
298
|
extractSession: extractSessionId ?? '',
|
|
293
299
|
});
|
|
294
300
|
const t0 = Date.now();
|
|
295
|
-
const r = await runStep(generateArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
301
|
+
const r = await runStep(generateArgs, cliPath, sessionId, { FORCE_COLOR: '1' }, true);
|
|
296
302
|
const durationMs = Date.now() - t0;
|
|
297
303
|
if (r.exitCode !== 0) {
|
|
298
304
|
updateStep(db, stepId, 'failed', {}, r.stderr);
|
|
@@ -355,7 +361,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
355
361
|
out: componentsPath,
|
|
356
362
|
});
|
|
357
363
|
const t0 = Date.now();
|
|
358
|
-
const r = await runStep(printArgs, cliPath);
|
|
364
|
+
const r = await runStep(printArgs, cliPath, sessionId);
|
|
359
365
|
const durationMs = Date.now() - t0;
|
|
360
366
|
if (r.exitCode !== 0) {
|
|
361
367
|
if (r.stderr)
|
|
@@ -425,7 +431,7 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
425
431
|
components: componentsPath,
|
|
426
432
|
});
|
|
427
433
|
const t0 = Date.now();
|
|
428
|
-
let r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
434
|
+
let r = await runStep(pushArgs, cliPath, sessionId, { FORCE_COLOR: '1' }, true);
|
|
429
435
|
const excludedByRetry = [];
|
|
430
436
|
let validationRetryCount = 0;
|
|
431
437
|
while (validationRetryCount < MAX_VALIDATION_RETRIES && isPreviewValidationError(r) && extractSessionId) {
|
|
@@ -442,10 +448,10 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
442
448
|
'--exclude-components',
|
|
443
449
|
offenders.join(','),
|
|
444
450
|
];
|
|
445
|
-
const rejectResult = await runStep(rejectArgs, cliPath);
|
|
451
|
+
const rejectResult = await runStep(rejectArgs, cliPath, sessionId);
|
|
446
452
|
if (rejectResult.exitCode !== 0)
|
|
447
453
|
break;
|
|
448
|
-
r = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
454
|
+
r = await runStep(pushArgs, cliPath, sessionId, { FORCE_COLOR: '1' }, true);
|
|
449
455
|
validationRetryCount++;
|
|
450
456
|
}
|
|
451
457
|
const durationMs = Date.now() - t0;
|
|
@@ -480,10 +486,10 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
480
486
|
'--exclude-components',
|
|
481
487
|
cycleNames.join(','),
|
|
482
488
|
];
|
|
483
|
-
const rejectResult = await runStep(rejectArgs, cliPath);
|
|
489
|
+
const rejectResult = await runStep(rejectArgs, cliPath, sessionId);
|
|
484
490
|
if (rejectResult.exitCode === 0) {
|
|
485
491
|
const retryT0 = Date.now();
|
|
486
|
-
const retryR = await runStep(pushArgs, cliPath, { FORCE_COLOR: '1' }, true);
|
|
492
|
+
const retryR = await runStep(pushArgs, cliPath, sessionId, { FORCE_COLOR: '1' }, true);
|
|
487
493
|
const retryDurationMs = Date.now() - t0 + (Date.now() - retryT0);
|
|
488
494
|
if (isSlotCycleError(retryR)) {
|
|
489
495
|
const retryReport = extractCycleReport(retryR.stderr);
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createProgram } from './program.js';
|
|
2
|
+
import { failActiveCommand, flushAnalytics } from './analytics/index.js';
|
|
2
3
|
createProgram()
|
|
3
4
|
.parseAsync()
|
|
4
|
-
.catch((err) => {
|
|
5
|
+
.catch(async (err) => {
|
|
6
|
+
await failActiveCommand({
|
|
7
|
+
error_name: err instanceof Error ? err.name : 'Error',
|
|
8
|
+
});
|
|
9
|
+
await flushAnalytics();
|
|
5
10
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
6
11
|
process.exit(1);
|
|
7
12
|
});
|
|
@@ -8,9 +8,10 @@ import { validateDTCGTokenFile } from './validate/validators/dtcg-validator.js';
|
|
|
8
8
|
import { formatDiagnostics } from './validate/validators/format-errors.js';
|
|
9
9
|
import { ValidateView } from './validate/tui/ValidateView.js';
|
|
10
10
|
import { getInteractiveTerminalSupport } from '../lib/terminal-capabilities.js';
|
|
11
|
-
|
|
11
|
+
import { bindAnalyticsSessionId, exitWithAnalytics } from '../analytics/index.js';
|
|
12
|
+
async function die(message) {
|
|
12
13
|
process.stderr.write(`${message}\n`);
|
|
13
|
-
|
|
14
|
+
return exitWithAnalytics(1);
|
|
14
15
|
}
|
|
15
16
|
async function pathExists(p) {
|
|
16
17
|
return access(p)
|
|
@@ -21,16 +22,16 @@ async function assertOutIsNotDirectory(outPath) {
|
|
|
21
22
|
if (await pathExists(outPath)) {
|
|
22
23
|
const s = await stat(outPath);
|
|
23
24
|
if (s.isDirectory())
|
|
24
|
-
die(`Error: --out must be a file path, not a directory: ${outPath}`);
|
|
25
|
+
await die(`Error: --out must be a file path, not a directory: ${outPath}`);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
function resolveSession(sessionFlag, command) {
|
|
28
|
+
async function resolveSession(sessionFlag, command) {
|
|
28
29
|
const db = openPipelineDb();
|
|
29
30
|
try {
|
|
30
31
|
const sessionId = sessionFlag ?? findLatestSessionForCommand(db, command);
|
|
31
32
|
if (!sessionId) {
|
|
32
33
|
const hint = command === 'generate components' ? 'generate components' : 'generate tokens';
|
|
33
|
-
die(`Error: no completed ${hint} session found. Run ${hint} first, or pass --session <id>.`);
|
|
34
|
+
return await die(`Error: no completed ${hint} session found. Run ${hint} first, or pass --session <id>.`);
|
|
34
35
|
}
|
|
35
36
|
return sessionId;
|
|
36
37
|
}
|
|
@@ -85,7 +86,8 @@ export function registerPrintCommand(program) {
|
|
|
85
86
|
.action(async (opts) => {
|
|
86
87
|
const outPath = resolve(opts.out);
|
|
87
88
|
await assertOutIsNotDirectory(outPath);
|
|
88
|
-
const sessionId = resolveSession(opts.session, 'generate components');
|
|
89
|
+
const sessionId = await resolveSession(opts.session, 'generate components');
|
|
90
|
+
await bindAnalyticsSessionId(sessionId);
|
|
89
91
|
const db = openPipelineDb();
|
|
90
92
|
let components;
|
|
91
93
|
let generateStepStatus = null;
|
|
@@ -110,13 +112,13 @@ export function registerPrintCommand(program) {
|
|
|
110
112
|
// component was rejected or left unresolved. This is a legitimate
|
|
111
113
|
// "clear the space" intent, but it's destructive, so require --allow-empty.
|
|
112
114
|
if (!opts.allowEmpty) {
|
|
113
|
-
die(`Error: all ${rejectedCount} generated component${rejectedCount === 1 ? ' was' : 's were'} rejected or left unresolved at final review in session '${sessionId}', so there is nothing to save. Accept at least one component (press [a] on a row, or [A] to accept all), or pass --allow-empty to write an empty manifest that will DELETE all components from the target space on push.`);
|
|
115
|
+
await die(`Error: all ${rejectedCount} generated component${rejectedCount === 1 ? ' was' : 's were'} rejected or left unresolved at final review in session '${sessionId}', so there is nothing to save. Accept at least one component (press [a] on a row, or [A] to accept all), or pass --allow-empty to write an empty manifest that will DELETE all components from the target space on push.`);
|
|
114
116
|
}
|
|
115
117
|
// Fall through: write an empty-but-present components manifest so a
|
|
116
118
|
// subsequent push removes every component from the target space.
|
|
117
119
|
}
|
|
118
120
|
else {
|
|
119
|
-
die(`Error: no generated components in session '${sessionId}'. Run generate components first.`);
|
|
121
|
+
await die(`Error: no generated components in session '${sessionId}'. Run generate components first.`);
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
if (generateStepStatus === 'failed') {
|
|
@@ -145,7 +147,8 @@ export function registerPrintCommand(program) {
|
|
|
145
147
|
.action(async (opts) => {
|
|
146
148
|
const outPath = resolve(opts.out);
|
|
147
149
|
await assertOutIsNotDirectory(outPath);
|
|
148
|
-
const sessionId = resolveSession(opts.session, 'generate tokens');
|
|
150
|
+
const sessionId = await resolveSession(opts.session, 'generate tokens');
|
|
151
|
+
await bindAnalyticsSessionId(sessionId);
|
|
149
152
|
const db = openPipelineDb();
|
|
150
153
|
let result;
|
|
151
154
|
try {
|
|
@@ -155,7 +158,7 @@ export function registerPrintCommand(program) {
|
|
|
155
158
|
db.close();
|
|
156
159
|
}
|
|
157
160
|
if (result.tokens.length === 0) {
|
|
158
|
-
die(`Error: no generated tokens in session '${sessionId}'. Run generate tokens first.`);
|
|
161
|
+
await die(`Error: no generated tokens in session '${sessionId}'. Run generate tokens first.`);
|
|
159
162
|
}
|
|
160
163
|
const tree = rebuildDTCGTree(result.groups, result.tokens);
|
|
161
164
|
await mkdir(resolve(outPath, '..'), { recursive: true });
|
|
@@ -171,7 +174,7 @@ export function registerPrintCommand(program) {
|
|
|
171
174
|
.action(async (opts) => {
|
|
172
175
|
if (!opts.components && !opts.tokens) {
|
|
173
176
|
process.stderr.write('Error: at least one of --components or --tokens is required.\n\nUsage: print validate [--components <path>] [--tokens <path>]\n');
|
|
174
|
-
|
|
177
|
+
await exitWithAnalytics(1);
|
|
175
178
|
}
|
|
176
179
|
const viewResults = [];
|
|
177
180
|
if (opts.components) {
|
|
@@ -199,7 +202,7 @@ export function registerPrintCommand(program) {
|
|
|
199
202
|
if (getInteractiveTerminalSupport().supported) {
|
|
200
203
|
const { waitUntilExit } = render(createElement(ValidateView, {
|
|
201
204
|
results: viewResults,
|
|
202
|
-
onExit: () =>
|
|
205
|
+
onExit: () => void exitWithAnalytics(exitCode),
|
|
203
206
|
}));
|
|
204
207
|
await waitUntilExit();
|
|
205
208
|
}
|
|
@@ -212,7 +215,7 @@ export function registerPrintCommand(program) {
|
|
|
212
215
|
}))
|
|
213
216
|
.join('\n\n');
|
|
214
217
|
process.stdout.write(output + '\n');
|
|
215
|
-
|
|
218
|
+
await exitWithAnalytics(exitCode);
|
|
216
219
|
}
|
|
217
220
|
});
|
|
218
221
|
}
|
package/dist/src/program.js
CHANGED
|
@@ -12,6 +12,8 @@ import { registerImportCommand } from './import/command.js';
|
|
|
12
12
|
import { registerSetupCommand } from './setup/command.js';
|
|
13
13
|
import { registerRunsCommand } from './runs/ls-command.js';
|
|
14
14
|
import { beginCommand } from './lib/debug-preamble.js';
|
|
15
|
+
import { completeActiveCommand, flushAnalytics, noteCommandStart, setPersistedAnalyticsDisabled, } from './analytics/index.js';
|
|
16
|
+
import { readExperiencesCredentials } from './credentials-store.js';
|
|
15
17
|
const require = createRequire(import.meta.url);
|
|
16
18
|
const pkg = require('../package.json');
|
|
17
19
|
/**
|
|
@@ -89,7 +91,15 @@ export function createProgram() {
|
|
|
89
91
|
const chain = [];
|
|
90
92
|
for (let c = actionCommand; c && c.parent; c = c.parent)
|
|
91
93
|
chain.unshift(c.name());
|
|
92
|
-
|
|
94
|
+
const commandChain = chain.join(' ') || actionCommand.name();
|
|
95
|
+
const { analyticsDisabled } = await readExperiencesCredentials();
|
|
96
|
+
setPersistedAnalyticsDisabled(analyticsDisabled ?? false);
|
|
97
|
+
noteCommandStart(commandChain);
|
|
98
|
+
await beginCommand(commandChain, { ...(debug !== undefined ? { debug } : {}) });
|
|
99
|
+
});
|
|
100
|
+
program.hook('postAction', async () => {
|
|
101
|
+
await completeActiveCommand();
|
|
102
|
+
await flushAnalytics();
|
|
93
103
|
});
|
|
94
104
|
return program;
|
|
95
105
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the operator whether to persist an opt-out of anonymous usage analytics.
|
|
3
|
+
*
|
|
4
|
+
* The helper is injectable so it can be unit-tested without a TTY. The caller
|
|
5
|
+
* provides an `ask` function that yields one line of input per call.
|
|
6
|
+
*
|
|
7
|
+
* Behavior:
|
|
8
|
+
* - Empty input returns `current` if defined, else `false` (default: enabled).
|
|
9
|
+
* - Input starting with 'y' or 'Y' returns `true` (disabled).
|
|
10
|
+
* - Input starting with 'n' or 'N' returns `false` (enabled).
|
|
11
|
+
* - Any other input falls back to the same rule as empty input.
|
|
12
|
+
*/
|
|
13
|
+
export declare function promptAnalyticsPreference(ask: (q: string) => Promise<string>, current?: boolean): Promise<boolean>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the operator whether to persist an opt-out of anonymous usage analytics.
|
|
3
|
+
*
|
|
4
|
+
* The helper is injectable so it can be unit-tested without a TTY. The caller
|
|
5
|
+
* provides an `ask` function that yields one line of input per call.
|
|
6
|
+
*
|
|
7
|
+
* Behavior:
|
|
8
|
+
* - Empty input returns `current` if defined, else `false` (default: enabled).
|
|
9
|
+
* - Input starting with 'y' or 'Y' returns `true` (disabled).
|
|
10
|
+
* - Input starting with 'n' or 'N' returns `false` (enabled).
|
|
11
|
+
* - Any other input falls back to the same rule as empty input.
|
|
12
|
+
*/
|
|
13
|
+
export async function promptAnalyticsPreference(ask, current) {
|
|
14
|
+
const defaultValue = current ?? false;
|
|
15
|
+
const hint = defaultValue ? '[Y/n]' : '[y/N]';
|
|
16
|
+
const answer = (await ask(` Disable anonymous usage analytics? ${hint} `)).trim().toLowerCase();
|
|
17
|
+
if (answer === '')
|
|
18
|
+
return defaultValue;
|
|
19
|
+
if (answer.startsWith('y'))
|
|
20
|
+
return true;
|
|
21
|
+
if (answer.startsWith('n'))
|
|
22
|
+
return false;
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
@@ -8,6 +8,7 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
import { readExperiencesCredentials, writeExperiencesCredentials, experiencesCredentialsPath, } from '../credentials-store.js';
|
|
9
9
|
import { promptAutoFilterPreference } from './auto-filter-prompt.js';
|
|
10
10
|
import { promptDebugModePreference } from './debug-mode-prompt.js';
|
|
11
|
+
import { promptAnalyticsPreference } from './analytics-prompt.js';
|
|
11
12
|
import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
const REQUIRED_NODE_MAJOR = 24;
|
|
@@ -629,6 +630,21 @@ async function setupQoL(profilePath) {
|
|
|
629
630
|
dim(' unchanged');
|
|
630
631
|
}
|
|
631
632
|
info('');
|
|
633
|
+
// 6d.1: Analytics opt-out
|
|
634
|
+
info('');
|
|
635
|
+
info('Anonymous usage analytics — helps us see which commands are used and where imports');
|
|
636
|
+
info('succeed or fail. Never includes source code, file paths, credentials, or authored content.');
|
|
637
|
+
info('Disabling here persists the opt-out; it will not silently re-enable later. See README > Usage data.');
|
|
638
|
+
const analyticsCreds = await readExperiencesCredentials();
|
|
639
|
+
const analyticsDisabled = await promptAnalyticsPreference((q) => prompt(q), analyticsCreds.analyticsDisabled);
|
|
640
|
+
if (analyticsDisabled !== (analyticsCreds.analyticsDisabled ?? false)) {
|
|
641
|
+
await writeExperiencesCredentials({ ...analyticsCreds, analyticsDisabled });
|
|
642
|
+
ok(`Analytics ${analyticsDisabled ? 'disabled' : 'enabled'}`);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
dim(' unchanged');
|
|
646
|
+
}
|
|
647
|
+
info('');
|
|
632
648
|
// 6e: NO_COLOR
|
|
633
649
|
info('');
|
|
634
650
|
info('NO_COLOR — set to 1 to disable ANSI color output (useful in CI or plain terminals).');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.24.1-dev-build-
|
|
3
|
+
"version": "2.24.1-dev-build-1db4d13.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,15 +28,16 @@
|
|
|
28
28
|
"prompts/"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@segment/analytics-node": "^3.0.0",
|
|
31
32
|
"commander": "^13.1.0",
|
|
32
33
|
"ink": "^4.4.1",
|
|
33
34
|
"react": "^18.3.1",
|
|
34
35
|
"react-devtools-core": "^4.19.1",
|
|
35
36
|
"react-dom": "^18.3.1",
|
|
36
|
-
"@contentful/experience-design-system-client": "2.24.1-dev-build-
|
|
37
|
-
"@contentful/experience-design-system-extraction": "2.24.1-dev-build-
|
|
38
|
-
"@contentful/experience-design-system-generation": "2.24.1-dev-build-
|
|
39
|
-
"@contentful/experience-design-system-types": "2.24.1-dev-build-
|
|
37
|
+
"@contentful/experience-design-system-client": "2.24.1-dev-build-1db4d13.0",
|
|
38
|
+
"@contentful/experience-design-system-extraction": "2.24.1-dev-build-1db4d13.0",
|
|
39
|
+
"@contentful/experience-design-system-generation": "2.24.1-dev-build-1db4d13.0",
|
|
40
|
+
"@contentful/experience-design-system-types": "2.24.1-dev-build-1db4d13.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@tsconfig/node24": "^24.0.4",
|