@contentful/experience-design-system-cli 2.24.1-dev-build-f3943ba.0 → 2.24.1-dev-build-5ff9f2f.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 +72 -50
- package/dist/src/apply/tui/ServerApplyView.js +1 -1
- 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 +25 -12
- package/dist/src/import/tui/steps/DoneStep.js +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/lib/contentful-urls.d.ts +3 -15
- package/dist/src/lib/contentful-urls.js +2 -16
- 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
|
@@ -11,6 +11,7 @@ import { buildRepoContextIndex, buildSelectionContext } from './context-builder.
|
|
|
11
11
|
import { runShowRationale } from './show-rationale.js';
|
|
12
12
|
import { isAbsolute, resolve } from 'node:path';
|
|
13
13
|
import { getDebugLogger } from '../../lib/debug-logger.js';
|
|
14
|
+
import { bindAnalyticsSessionId, enrichCommandResult, exitWithAnalytics } from '../../analytics/index.js';
|
|
14
15
|
import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
|
|
15
16
|
const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
|
|
16
17
|
export const DEFAULT_CONCURRENCY = 10;
|
|
@@ -27,7 +28,7 @@ function resolveBatchSize() {
|
|
|
27
28
|
return DEFAULT_BATCH_SIZE;
|
|
28
29
|
return Math.floor(n);
|
|
29
30
|
}
|
|
30
|
-
function resolveSessionId(sessionFlag) {
|
|
31
|
+
async function resolveSessionId(sessionFlag) {
|
|
31
32
|
if (sessionFlag)
|
|
32
33
|
return sessionFlag;
|
|
33
34
|
const db = openPipelineDb();
|
|
@@ -42,7 +43,7 @@ function resolveSessionId(sessionFlag) {
|
|
|
42
43
|
.get();
|
|
43
44
|
if (!row) {
|
|
44
45
|
process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
|
|
45
|
-
|
|
46
|
+
return await exitWithAnalytics(1);
|
|
46
47
|
}
|
|
47
48
|
return row.id;
|
|
48
49
|
}
|
|
@@ -329,7 +330,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
329
330
|
catch (err) {
|
|
330
331
|
const message = err instanceof Error ? err.message : String(err);
|
|
331
332
|
process.stderr.write(`Error: ${message}\n`);
|
|
332
|
-
|
|
333
|
+
await exitWithAnalytics(1);
|
|
333
334
|
return;
|
|
334
335
|
}
|
|
335
336
|
}
|
|
@@ -338,7 +339,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
338
339
|
const model = opts.model ?? savedCreds.agentModel;
|
|
339
340
|
if (!agentName || !isAgentName(agentName)) {
|
|
340
341
|
process.stderr.write(`Error: no agent configured. Pass --agent <name> or run experiences setup. Accepted values: ${AGENT_NAMES.join(', ')}\n`);
|
|
341
|
-
|
|
342
|
+
await exitWithAnalytics(1);
|
|
342
343
|
return;
|
|
343
344
|
}
|
|
344
345
|
const agent = agentName;
|
|
@@ -352,7 +353,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
352
353
|
.catch(() => false);
|
|
353
354
|
if (!exists) {
|
|
354
355
|
process.stderr.write(`Error: custom prompt path not found: ${resolvedPath}\n`);
|
|
355
|
-
|
|
356
|
+
await exitWithAnalytics(1);
|
|
356
357
|
return;
|
|
357
358
|
}
|
|
358
359
|
if (!selectPromptPath.toLowerCase().endsWith('.md')) {
|
|
@@ -360,7 +361,8 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
360
361
|
}
|
|
361
362
|
process.stderr.write(formatCustomPromptBanner('select', resolvedPath));
|
|
362
363
|
}
|
|
363
|
-
const sessionId = resolveSessionId(opts.session);
|
|
364
|
+
const sessionId = await resolveSessionId(opts.session);
|
|
365
|
+
await bindAnalyticsSessionId(sessionId);
|
|
364
366
|
const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
|
|
365
367
|
const db = openPipelineDb();
|
|
366
368
|
let rawComponents;
|
|
@@ -374,7 +376,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
374
376
|
}
|
|
375
377
|
if (rawComponents.length === 0) {
|
|
376
378
|
process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
|
|
377
|
-
|
|
379
|
+
await exitWithAnalytics(1);
|
|
378
380
|
return;
|
|
379
381
|
}
|
|
380
382
|
// Re-run validation (not persisted to DB, so always recompute).
|
|
@@ -399,7 +401,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
399
401
|
lines.push('');
|
|
400
402
|
lines.push('Re-run with --exclude-invalid to auto-reject these components, or fix them in source first.');
|
|
401
403
|
process.stderr.write(lines.join('\n') + '\n');
|
|
402
|
-
|
|
404
|
+
await exitWithAnalytics(1);
|
|
403
405
|
return;
|
|
404
406
|
}
|
|
405
407
|
const componentsForAgent = validatedComponents.filter((comp) => !shouldExcludeDueToValidation(comp));
|
|
@@ -426,7 +428,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
426
428
|
if (opts.dryRun) {
|
|
427
429
|
if (selectionCandidates.length === 0) {
|
|
428
430
|
process.stderr.write('No valid components to preview — all components were excluded due to validation errors.\n');
|
|
429
|
-
|
|
431
|
+
await exitWithAnalytics(0);
|
|
430
432
|
return;
|
|
431
433
|
}
|
|
432
434
|
const first = selectionCandidates[0];
|
|
@@ -438,7 +440,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
438
440
|
skillPathOverride: selectPromptPath ? resolve(selectPromptPath) : undefined,
|
|
439
441
|
});
|
|
440
442
|
process.stdout.write(prompt + '\n');
|
|
441
|
-
|
|
443
|
+
await exitWithAnalytics(0);
|
|
442
444
|
return;
|
|
443
445
|
}
|
|
444
446
|
// --no-cache is the global kill-switch; --no-select-cache is the stage-
|
|
@@ -504,7 +506,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
504
506
|
}
|
|
505
507
|
catch (error) {
|
|
506
508
|
process.stderr.write(`Error: unable to initialize select session.\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
507
|
-
|
|
509
|
+
await exitWithAnalytics(1);
|
|
508
510
|
return;
|
|
509
511
|
}
|
|
510
512
|
const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
|
|
@@ -550,6 +552,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
550
552
|
finally {
|
|
551
553
|
stepDb.close();
|
|
552
554
|
}
|
|
555
|
+
enrichCommandResult({ accepted_component_count: accepted.length });
|
|
553
556
|
process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length} Needs review: ${unresolved.length}\n`);
|
|
554
557
|
});
|
|
555
558
|
}
|
|
@@ -45,6 +45,9 @@ export declare class ImportApiClient {
|
|
|
45
45
|
private spaceId;
|
|
46
46
|
private environmentId;
|
|
47
47
|
private retry;
|
|
48
|
+
private lastRequestId?;
|
|
49
|
+
getLastRequestId(): string | undefined;
|
|
50
|
+
private noteRequestId;
|
|
48
51
|
constructor(opts: ApiClientOptions);
|
|
49
52
|
private base;
|
|
50
53
|
private headers;
|
|
@@ -180,6 +180,15 @@ export class ImportApiClient {
|
|
|
180
180
|
spaceId;
|
|
181
181
|
environmentId;
|
|
182
182
|
retry;
|
|
183
|
+
lastRequestId;
|
|
184
|
+
getLastRequestId() {
|
|
185
|
+
return this.lastRequestId;
|
|
186
|
+
}
|
|
187
|
+
noteRequestId(response) {
|
|
188
|
+
const requestId = response.headers.get('x-contentful-request-id');
|
|
189
|
+
if (requestId)
|
|
190
|
+
this.lastRequestId = requestId;
|
|
191
|
+
}
|
|
183
192
|
constructor(opts) {
|
|
184
193
|
this.host = toApiHost(opts.host);
|
|
185
194
|
this.token = opts.cmaToken;
|
|
@@ -237,8 +246,10 @@ export class ImportApiClient {
|
|
|
237
246
|
await this.retry.sleep(delayMs);
|
|
238
247
|
continue;
|
|
239
248
|
}
|
|
240
|
-
if (!isTransientStatus(result.response.status))
|
|
249
|
+
if (!isTransientStatus(result.response.status)) {
|
|
250
|
+
this.noteRequestId(result.response);
|
|
241
251
|
return result;
|
|
252
|
+
}
|
|
242
253
|
if (attempt === this.retry.maxAttempts) {
|
|
243
254
|
const body = stringifyError(result.error);
|
|
244
255
|
const guidance = `The ${phase} request failed after ${attempt} attempts because the service remained unavailable. ` +
|
|
@@ -273,6 +284,7 @@ export class ImportApiClient {
|
|
|
273
284
|
// false positives that don't apply to the design-systems API authorization path.
|
|
274
285
|
const url = `${this.host}/users/me`;
|
|
275
286
|
const res = await request(url, { token: this.token });
|
|
287
|
+
this.noteRequestId(res);
|
|
276
288
|
if (res.status === 401) {
|
|
277
289
|
throw new ApiError('CMA token is invalid or revoked', res.status, await res.text());
|
|
278
290
|
}
|
|
@@ -298,6 +310,7 @@ export class ImportApiClient {
|
|
|
298
310
|
catch {
|
|
299
311
|
return; // network error — don't block; the real call will surface it
|
|
300
312
|
}
|
|
313
|
+
this.noteRequestId(res);
|
|
301
314
|
if (res.ok)
|
|
302
315
|
return;
|
|
303
316
|
if (res.status >= 500)
|
|
@@ -11,7 +11,7 @@ export declare function formatSlotCycleReport(cycles: ReturnType<typeof findSlot
|
|
|
11
11
|
export declare function assertNoSlotCycles(components: Array<{
|
|
12
12
|
key: string;
|
|
13
13
|
entry: CDFComponentEntry;
|
|
14
|
-
}>): void
|
|
14
|
+
}>): Promise<void>;
|
|
15
15
|
export declare function extractComponentsFromManifest(manifest: {
|
|
16
16
|
componentsManifest?: Record<string, unknown>;
|
|
17
17
|
} | null | undefined): Array<{
|
|
@@ -16,9 +16,10 @@ import { addAllowDeletionsOption, addArtifactInputOptions, addCompositionOptions
|
|
|
16
16
|
import { stripAllowedComponents } from '../import/strip-allowed-components.js';
|
|
17
17
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
18
18
|
import { getInteractiveTerminalSupport, requireInteractiveTerminal } from '../lib/terminal-capabilities.js';
|
|
19
|
-
|
|
19
|
+
import { bindAnalyticsSessionId, exitWithAnalytics, failureFromApiError, recordApplyOutcome, recordContentfulContext, } from '../analytics/index.js';
|
|
20
|
+
async function die(message, fields = {}) {
|
|
20
21
|
process.stderr.write(`${message}\n`);
|
|
21
|
-
|
|
22
|
+
return exitWithAnalytics(1, fields);
|
|
22
23
|
}
|
|
23
24
|
async function pathExists(p) {
|
|
24
25
|
return access(p)
|
|
@@ -27,7 +28,7 @@ async function pathExists(p) {
|
|
|
27
28
|
}
|
|
28
29
|
async function assertFileExists(flag, p) {
|
|
29
30
|
if (!(await pathExists(p)))
|
|
30
|
-
die(`Error: file not found: ${p} (from ${flag})`);
|
|
31
|
+
return await die(`Error: file not found: ${p} (from ${flag})`);
|
|
31
32
|
}
|
|
32
33
|
async function readJsonFile(flag, p) {
|
|
33
34
|
let text;
|
|
@@ -35,13 +36,13 @@ async function readJsonFile(flag, p) {
|
|
|
35
36
|
text = await readFile(p, 'utf8');
|
|
36
37
|
}
|
|
37
38
|
catch {
|
|
38
|
-
die(`Error: file not found: ${p} (from ${flag})`);
|
|
39
|
+
return await die(`Error: file not found: ${p} (from ${flag})`);
|
|
39
40
|
}
|
|
40
41
|
try {
|
|
41
42
|
return JSON.parse(text);
|
|
42
43
|
}
|
|
43
44
|
catch {
|
|
44
|
-
die(`Error: ${flag} is not valid JSON: ${p}`);
|
|
45
|
+
return await die(`Error: ${flag} is not valid JSON: ${p}`);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
const IGNORE_TOKEN_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.nuxt', '.git']);
|
|
@@ -83,12 +84,12 @@ export async function readTokensFromPath(flag, p) {
|
|
|
83
84
|
s = await stat(p);
|
|
84
85
|
}
|
|
85
86
|
catch {
|
|
86
|
-
die(`Error: file not found: ${p} (from ${flag})`);
|
|
87
|
+
return await die(`Error: file not found: ${p} (from ${flag})`);
|
|
87
88
|
}
|
|
88
89
|
if (s.isDirectory()) {
|
|
89
90
|
const files = await collectJsonFiles(p);
|
|
90
91
|
if (files.length === 0)
|
|
91
|
-
die(`Error: no .json files found in directory: ${p} (from ${flag})`);
|
|
92
|
+
return await die(`Error: no .json files found in directory: ${p} (from ${flag})`);
|
|
92
93
|
const merged = {};
|
|
93
94
|
for (const file of files.sort()) {
|
|
94
95
|
let text;
|
|
@@ -111,36 +112,36 @@ export async function readTokensFromPath(flag, p) {
|
|
|
111
112
|
}
|
|
112
113
|
const { valid, errors } = validateDTCG(merged);
|
|
113
114
|
if (!valid)
|
|
114
|
-
die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
|
|
115
|
+
return await die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
|
|
115
116
|
return flattenDTCG(merged, '');
|
|
116
117
|
}
|
|
117
118
|
const raw = await readJsonFile(flag, p);
|
|
118
119
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
119
|
-
die(`Error: ${flag} is not valid JSON: expected an object`);
|
|
120
|
+
return await die(`Error: ${flag} is not valid JSON: expected an object`);
|
|
120
121
|
}
|
|
121
122
|
const { valid, errors } = validateDTCG(raw);
|
|
122
123
|
if (!valid)
|
|
123
|
-
die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
|
|
124
|
+
return await die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
|
|
124
125
|
return flattenDTCG(raw, '');
|
|
125
126
|
}
|
|
126
127
|
async function resolveSharedInputs(opts) {
|
|
127
128
|
if (!opts.components && !opts.tokens && !opts.session) {
|
|
128
|
-
die('Error: at least one of --components, --tokens, or --session is required');
|
|
129
|
+
return await die('Error: at least one of --components, --tokens, or --session is required');
|
|
129
130
|
}
|
|
130
131
|
if (opts.session && opts.components) {
|
|
131
|
-
die('Error: --session and --components are mutually exclusive');
|
|
132
|
+
return await die('Error: --session and --components are mutually exclusive');
|
|
132
133
|
}
|
|
133
134
|
const spaceId = opts.spaceId ?? process.env.CONTENTFUL_SPACE_ID;
|
|
134
135
|
const environmentId = opts.environmentId ?? process.env.CONTENTFUL_ENVIRONMENT_ID;
|
|
135
136
|
if (!spaceId)
|
|
136
|
-
die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
|
|
137
|
+
return await die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
|
|
137
138
|
if (!environmentId)
|
|
138
|
-
die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
|
|
139
|
+
return await die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
|
|
139
140
|
opts.spaceId = spaceId;
|
|
140
141
|
opts.environmentId = environmentId;
|
|
141
142
|
const cmaToken = opts.cmaToken ?? process.env.CONTENTFUL_MANAGEMENT_TOKEN;
|
|
142
143
|
if (!cmaToken) {
|
|
143
|
-
die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
|
|
144
|
+
return await die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
|
|
144
145
|
}
|
|
145
146
|
if (opts.components)
|
|
146
147
|
await assertFileExists('--components', opts.components);
|
|
@@ -154,14 +155,14 @@ async function resolveSharedInputs(opts) {
|
|
|
154
155
|
db.close();
|
|
155
156
|
}
|
|
156
157
|
if (components.length === 0) {
|
|
157
|
-
die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
|
|
158
|
+
return await die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
else if (opts.components) {
|
|
161
162
|
const raw = await readJsonFile('--components', opts.components);
|
|
162
163
|
const result = validateCDF(raw);
|
|
163
164
|
if (!result.valid) {
|
|
164
|
-
die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
|
|
165
|
+
return await die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
|
|
165
166
|
}
|
|
166
167
|
components = result.components;
|
|
167
168
|
}
|
|
@@ -213,12 +214,12 @@ export function formatSlotCycleReport(cycles) {
|
|
|
213
214
|
}
|
|
214
215
|
return lines;
|
|
215
216
|
}
|
|
216
|
-
export function assertNoSlotCycles(components) {
|
|
217
|
+
export async function assertNoSlotCycles(components) {
|
|
217
218
|
const cycles = detectSlotCycles(components);
|
|
218
219
|
if (cycles.length === 0)
|
|
219
220
|
return;
|
|
220
221
|
process.stderr.write(formatSlotCycleReport(cycles).join('\n') + '\n');
|
|
221
|
-
|
|
222
|
+
await exitWithAnalytics(1);
|
|
222
223
|
}
|
|
223
224
|
export function extractComponentsFromManifest(manifest) {
|
|
224
225
|
const componentsManifest = manifest?.componentsManifest;
|
|
@@ -287,6 +288,7 @@ function buildApplyOutput(operation, spaceId, environmentId, host) {
|
|
|
287
288
|
componentTypes: countByAction(componentItems),
|
|
288
289
|
designTokens: countByAction(tokenItems),
|
|
289
290
|
viewUrl: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId }),
|
|
291
|
+
tokensUrl: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId, view: 'design_tokens' }),
|
|
290
292
|
failures: items
|
|
291
293
|
.filter((item) => item.status === 'failed')
|
|
292
294
|
.map((item) => ({
|
|
@@ -427,18 +429,24 @@ export function registerApplyCommand(program) {
|
|
|
427
429
|
}
|
|
428
430
|
catch (e) {
|
|
429
431
|
if (e instanceof ApiError)
|
|
430
|
-
die(`Error: ${formatApiError(e)}
|
|
432
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
431
433
|
throw e;
|
|
432
434
|
}
|
|
433
435
|
const { components, tokens, client } = inputs;
|
|
436
|
+
const spaceId = opts.spaceId;
|
|
437
|
+
const environmentId = opts.environmentId;
|
|
438
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
439
|
+
space_key: spaceId,
|
|
440
|
+
environment_key: environmentId,
|
|
441
|
+
});
|
|
434
442
|
try {
|
|
435
443
|
await client.validateToken();
|
|
436
444
|
}
|
|
437
445
|
catch (e) {
|
|
438
446
|
if (e instanceof ApiError)
|
|
439
|
-
die(`Error: ${formatApiError(e)}
|
|
447
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
440
448
|
const cause = e instanceof Error && e.cause instanceof Error ? e.cause.message : '';
|
|
441
|
-
die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
|
|
449
|
+
return await die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
|
|
442
450
|
}
|
|
443
451
|
const manifest = buildManifest(components, tokens);
|
|
444
452
|
let preview;
|
|
@@ -447,11 +455,10 @@ export function registerApplyCommand(program) {
|
|
|
447
455
|
}
|
|
448
456
|
catch (e) {
|
|
449
457
|
if (e instanceof ApiError)
|
|
450
|
-
die(`Error: ${formatApiError(e)}
|
|
458
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
451
459
|
throw e;
|
|
452
460
|
}
|
|
453
|
-
|
|
454
|
-
const environmentId = opts.environmentId;
|
|
461
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
455
462
|
if (getInteractiveTerminalSupport().supported) {
|
|
456
463
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
457
464
|
preview,
|
|
@@ -463,7 +470,7 @@ export function registerApplyCommand(program) {
|
|
|
463
470
|
}
|
|
464
471
|
else {
|
|
465
472
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
466
|
-
|
|
473
|
+
await exitWithAnalytics(0);
|
|
467
474
|
}
|
|
468
475
|
});
|
|
469
476
|
const pushCmd = applyCmd.command('push').description('Write component types and design tokens to Contentful ExO');
|
|
@@ -480,7 +487,7 @@ export function registerApplyCommand(program) {
|
|
|
480
487
|
const isTTY = getInteractiveTerminalSupport().supported;
|
|
481
488
|
if (!isTTY && !opts.yes) {
|
|
482
489
|
process.stderr.write('Error: apply push requires --yes in non-interactive mode\n');
|
|
483
|
-
|
|
490
|
+
await exitWithAnalytics(1);
|
|
484
491
|
}
|
|
485
492
|
let inputs;
|
|
486
493
|
try {
|
|
@@ -488,17 +495,23 @@ export function registerApplyCommand(program) {
|
|
|
488
495
|
}
|
|
489
496
|
catch (e) {
|
|
490
497
|
if (e instanceof ApiError)
|
|
491
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
498
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
492
499
|
throw e;
|
|
493
500
|
}
|
|
494
501
|
const { components, tokens, client } = inputs;
|
|
495
|
-
|
|
502
|
+
const spaceId = opts.spaceId;
|
|
503
|
+
const environmentId = opts.environmentId;
|
|
504
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
505
|
+
space_key: spaceId,
|
|
506
|
+
environment_key: environmentId,
|
|
507
|
+
});
|
|
508
|
+
await assertNoSlotCycles(components);
|
|
496
509
|
try {
|
|
497
510
|
await client.validateToken();
|
|
498
511
|
}
|
|
499
512
|
catch (e) {
|
|
500
513
|
if (e instanceof ApiError)
|
|
501
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
514
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
502
515
|
throw e;
|
|
503
516
|
}
|
|
504
517
|
const manifest = buildManifest(components, tokens);
|
|
@@ -508,11 +521,10 @@ export function registerApplyCommand(program) {
|
|
|
508
521
|
}
|
|
509
522
|
catch (e) {
|
|
510
523
|
if (e instanceof ApiError)
|
|
511
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
524
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
512
525
|
throw e;
|
|
513
526
|
}
|
|
514
|
-
|
|
515
|
-
const environmentId = opts.environmentId;
|
|
527
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
516
528
|
if (opts.dryRun) {
|
|
517
529
|
if (isTTY) {
|
|
518
530
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
@@ -526,7 +538,7 @@ export function registerApplyCommand(program) {
|
|
|
526
538
|
else {
|
|
527
539
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
528
540
|
}
|
|
529
|
-
|
|
541
|
+
await exitWithAnalytics(0);
|
|
530
542
|
}
|
|
531
543
|
if (isEmptyPreview(preview)) {
|
|
532
544
|
if (isTTY && !opts.yes) {
|
|
@@ -535,14 +547,14 @@ export function registerApplyCommand(program) {
|
|
|
535
547
|
else {
|
|
536
548
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
537
549
|
}
|
|
538
|
-
|
|
550
|
+
await exitWithAnalytics(0);
|
|
539
551
|
}
|
|
540
552
|
const breakingWithImpact = hasBreakingChangesWithImpact(preview);
|
|
541
553
|
if (!isTTY || opts.yes) {
|
|
542
554
|
if (breakingWithImpact && !opts.force) {
|
|
543
555
|
process.stderr.write('Error: breaking changes with downstream impact detected. Use --force to acknowledge.\n');
|
|
544
556
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
545
|
-
|
|
557
|
+
await exitWithAnalytics(1);
|
|
546
558
|
}
|
|
547
559
|
const verbose = opts.verbose ?? false;
|
|
548
560
|
if (verbose) {
|
|
@@ -557,7 +569,7 @@ export function registerApplyCommand(program) {
|
|
|
557
569
|
}
|
|
558
570
|
catch (e) {
|
|
559
571
|
if (e instanceof ApiError)
|
|
560
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
572
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
561
573
|
throw e;
|
|
562
574
|
}
|
|
563
575
|
process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
|
|
@@ -566,12 +578,14 @@ export function registerApplyCommand(program) {
|
|
|
566
578
|
}
|
|
567
579
|
catch (e) {
|
|
568
580
|
if (e instanceof ApiError)
|
|
569
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
581
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
570
582
|
throw e;
|
|
571
583
|
}
|
|
572
584
|
const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
|
|
585
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
573
586
|
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
574
|
-
|
|
587
|
+
const exitCode = operation.sys.status === 'succeeded' ? 0 : 1;
|
|
588
|
+
await exitWithAnalytics(exitCode);
|
|
575
589
|
return;
|
|
576
590
|
}
|
|
577
591
|
await new Promise((resolvePromise) => {
|
|
@@ -621,6 +635,7 @@ export function registerApplyCommand(program) {
|
|
|
621
635
|
}
|
|
622
636
|
throw e;
|
|
623
637
|
}
|
|
638
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
624
639
|
instance.rerender(createElement(ServerApplyDone, {
|
|
625
640
|
operation,
|
|
626
641
|
spaceId,
|
|
@@ -639,7 +654,7 @@ export function registerApplyCommand(program) {
|
|
|
639
654
|
void runApply(acknowledge, applyDeletions);
|
|
640
655
|
},
|
|
641
656
|
onCancel: () => {
|
|
642
|
-
|
|
657
|
+
void exitWithAnalytics(0);
|
|
643
658
|
},
|
|
644
659
|
}));
|
|
645
660
|
void instance.waitUntilExit().then(() => resolvePromise());
|
|
@@ -664,17 +679,17 @@ export function registerApplyCommand(program) {
|
|
|
664
679
|
}
|
|
665
680
|
catch (e) {
|
|
666
681
|
if (e instanceof ApiError)
|
|
667
|
-
die(`Error: ${formatApiError(e)}
|
|
682
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
668
683
|
throw e;
|
|
669
684
|
}
|
|
670
685
|
const { components, tokens, client } = inputs;
|
|
671
|
-
assertNoSlotCycles(components);
|
|
686
|
+
await assertNoSlotCycles(components);
|
|
672
687
|
try {
|
|
673
688
|
await client.validateToken();
|
|
674
689
|
}
|
|
675
690
|
catch (e) {
|
|
676
691
|
if (e instanceof ApiError)
|
|
677
|
-
die(`Error: ${formatApiError(e)}
|
|
692
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
678
693
|
throw e;
|
|
679
694
|
}
|
|
680
695
|
const fullManifest = buildManifest(components, tokens);
|
|
@@ -684,21 +699,26 @@ export function registerApplyCommand(program) {
|
|
|
684
699
|
}
|
|
685
700
|
catch (e) {
|
|
686
701
|
if (e instanceof ApiError)
|
|
687
|
-
die(`Error: ${formatApiError(e)}
|
|
702
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
688
703
|
throw e;
|
|
689
704
|
}
|
|
690
705
|
const spaceId = opts.spaceId;
|
|
691
706
|
const environmentId = opts.environmentId;
|
|
707
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
708
|
+
space_key: spaceId,
|
|
709
|
+
environment_key: environmentId,
|
|
710
|
+
});
|
|
711
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
692
712
|
const entities = getSelectableEntities(preview);
|
|
693
713
|
if (entities.length === 0) {
|
|
694
714
|
process.stderr.write('Nothing to change — design system is up to date.\n');
|
|
695
|
-
|
|
715
|
+
await exitWithAnalytics(0);
|
|
696
716
|
}
|
|
697
717
|
if (nonInteractive) {
|
|
698
718
|
const selectedKeys = resolveNonInteractiveSelection(entities, opts);
|
|
699
719
|
if (selectedKeys.size === 0) {
|
|
700
720
|
process.stderr.write('No entities matched selection criteria.\n');
|
|
701
|
-
|
|
721
|
+
await exitWithAnalytics(0);
|
|
702
722
|
}
|
|
703
723
|
const selectedComponentKeys = new Set();
|
|
704
724
|
const selectedTokenPaths = new Set();
|
|
@@ -714,7 +734,7 @@ export function registerApplyCommand(program) {
|
|
|
714
734
|
const hasBreaking = entities.some((e) => e.isBreaking && selectedKeys.has(makeSelectKey(e.kind, e.id)));
|
|
715
735
|
if (hasBreaking && !opts.force) {
|
|
716
736
|
process.stderr.write('Error: selection includes breaking changes. Use --force to acknowledge.\n');
|
|
717
|
-
|
|
737
|
+
await exitWithAnalytics(1);
|
|
718
738
|
}
|
|
719
739
|
let operation;
|
|
720
740
|
try {
|
|
@@ -725,7 +745,7 @@ export function registerApplyCommand(program) {
|
|
|
725
745
|
}
|
|
726
746
|
catch (e) {
|
|
727
747
|
if (e instanceof ApiError)
|
|
728
|
-
die(`Error: ${formatApiError(e)}
|
|
748
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
729
749
|
throw e;
|
|
730
750
|
}
|
|
731
751
|
process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
|
|
@@ -734,12 +754,13 @@ export function registerApplyCommand(program) {
|
|
|
734
754
|
}
|
|
735
755
|
catch (e) {
|
|
736
756
|
if (e instanceof ApiError)
|
|
737
|
-
die(`Error: ${formatApiError(e)}
|
|
757
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
738
758
|
throw e;
|
|
739
759
|
}
|
|
740
760
|
const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
|
|
761
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
741
762
|
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
742
|
-
|
|
763
|
+
await exitWithAnalytics(operation.sys.status === 'succeeded' ? 0 : 1);
|
|
743
764
|
return;
|
|
744
765
|
}
|
|
745
766
|
await new Promise((resolvePromise) => {
|
|
@@ -801,6 +822,7 @@ export function registerApplyCommand(program) {
|
|
|
801
822
|
}
|
|
802
823
|
throw e;
|
|
803
824
|
}
|
|
825
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
804
826
|
instance.rerender(createElement(ServerApplyDone, {
|
|
805
827
|
operation,
|
|
806
828
|
spaceId,
|
|
@@ -42,7 +42,7 @@ export function ServerApplyDone({ operation, spaceId, environmentId, host }) {
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
const failures = (operation.items ?? []).filter((item) => item.status === 'failed');
|
|
45
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), operation.sys.status === 'succeeded' && operation.summary.succeeded > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " View your design system:" }), _jsxs(Text, { color: "cyan", children: [" ", buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), operation.sys.status === 'succeeded' && operation.summary.succeeded > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " View your design system:" }), _jsxs(Text, { color: "cyan", children: [" ", buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId })] }), _jsx(Text, { dimColor: true, children: " View your design tokens:" }), _jsxs(Text, { color: "cyan", children: [' ', buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId, view: 'design_tokens' })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
|
|
46
46
|
}
|
|
47
47
|
function formatItemError(error) {
|
|
48
48
|
return formatEdsiError(error);
|
|
@@ -13,6 +13,8 @@ export type ExperiencesCredentials = {
|
|
|
13
13
|
autoFilter?: boolean;
|
|
14
14
|
/** Feature: default debug-mode (writes JSONL trace of every decision) for all commands. */
|
|
15
15
|
debug?: boolean;
|
|
16
|
+
/** Persisted opt-out for anonymous CLI usage analytics, set via `experiences setup`. */
|
|
17
|
+
analyticsDisabled?: boolean;
|
|
16
18
|
/**
|
|
17
19
|
* Feature (atomic mode): default composition mode. `atomic` (default) imports
|
|
18
20
|
* flat components with no embedded-component hierarchy; `composite` opts into
|
|
@@ -36,6 +36,7 @@ export async function readExperiencesCredentials() {
|
|
|
36
36
|
...(parsed.generatePromptPath ? { generatePromptPath: parsed.generatePromptPath } : {}),
|
|
37
37
|
...(typeof parsed.autoFilter === 'boolean' ? { autoFilter: parsed.autoFilter } : {}),
|
|
38
38
|
...(typeof parsed.debug === 'boolean' ? { debug: parsed.debug } : {}),
|
|
39
|
+
...(typeof parsed.analyticsDisabled === 'boolean' ? { analyticsDisabled: parsed.analyticsDisabled } : {}),
|
|
39
40
|
...(typeof parsed.compositionMode === 'string' && isCompositionMode(parsed.compositionMode)
|
|
40
41
|
? { compositionMode: parsed.compositionMode }
|
|
41
42
|
: {}),
|
|
@@ -52,7 +53,7 @@ export async function readExperiencesCredentials() {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
export async function writeExperiencesCredentials(creds) {
|
|
55
|
-
const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, compositionMode, ...rest } = creds;
|
|
56
|
+
const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, analyticsDisabled, compositionMode, ...rest } = creds;
|
|
56
57
|
const host = toConfiguredHost(creds.host);
|
|
57
58
|
await mkdir(CREDENTIALS_DIR, { recursive: true });
|
|
58
59
|
await writeFile(CREDENTIALS_PATH, JSON.stringify({
|
|
@@ -64,6 +65,7 @@ export async function writeExperiencesCredentials(creds) {
|
|
|
64
65
|
...(generatePromptPath ? { generatePromptPath } : {}),
|
|
65
66
|
...(typeof autoFilter === 'boolean' ? { autoFilter } : {}),
|
|
66
67
|
...(typeof debug === 'boolean' ? { debug } : {}),
|
|
68
|
+
...(typeof analyticsDisabled === 'boolean' ? { analyticsDisabled } : {}),
|
|
67
69
|
...(compositionMode && isCompositionMode(compositionMode) ? { compositionMode } : {}),
|
|
68
70
|
}, null, 2) + '\n', { mode: 0o600 });
|
|
69
71
|
}
|