@contentful/experience-design-system-cli 2.24.0 → 2.24.1-dev-build-58e271c.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 +23 -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 +7 -0
- package/dist/src/analytics/client.js +68 -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/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 +8 -1
- 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;
|
|
@@ -427,18 +428,24 @@ export function registerApplyCommand(program) {
|
|
|
427
428
|
}
|
|
428
429
|
catch (e) {
|
|
429
430
|
if (e instanceof ApiError)
|
|
430
|
-
die(`Error: ${formatApiError(e)}
|
|
431
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
431
432
|
throw e;
|
|
432
433
|
}
|
|
433
434
|
const { components, tokens, client } = inputs;
|
|
435
|
+
const spaceId = opts.spaceId;
|
|
436
|
+
const environmentId = opts.environmentId;
|
|
437
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
438
|
+
space_key: spaceId,
|
|
439
|
+
environment_key: environmentId,
|
|
440
|
+
});
|
|
434
441
|
try {
|
|
435
442
|
await client.validateToken();
|
|
436
443
|
}
|
|
437
444
|
catch (e) {
|
|
438
445
|
if (e instanceof ApiError)
|
|
439
|
-
die(`Error: ${formatApiError(e)}
|
|
446
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
440
447
|
const cause = e instanceof Error && e.cause instanceof Error ? e.cause.message : '';
|
|
441
|
-
die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
|
|
448
|
+
return await die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
|
|
442
449
|
}
|
|
443
450
|
const manifest = buildManifest(components, tokens);
|
|
444
451
|
let preview;
|
|
@@ -447,11 +454,10 @@ export function registerApplyCommand(program) {
|
|
|
447
454
|
}
|
|
448
455
|
catch (e) {
|
|
449
456
|
if (e instanceof ApiError)
|
|
450
|
-
die(`Error: ${formatApiError(e)}
|
|
457
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
451
458
|
throw e;
|
|
452
459
|
}
|
|
453
|
-
|
|
454
|
-
const environmentId = opts.environmentId;
|
|
460
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
455
461
|
if (getInteractiveTerminalSupport().supported) {
|
|
456
462
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
457
463
|
preview,
|
|
@@ -463,7 +469,7 @@ export function registerApplyCommand(program) {
|
|
|
463
469
|
}
|
|
464
470
|
else {
|
|
465
471
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
466
|
-
|
|
472
|
+
await exitWithAnalytics(0);
|
|
467
473
|
}
|
|
468
474
|
});
|
|
469
475
|
const pushCmd = applyCmd.command('push').description('Write component types and design tokens to Contentful ExO');
|
|
@@ -480,7 +486,7 @@ export function registerApplyCommand(program) {
|
|
|
480
486
|
const isTTY = getInteractiveTerminalSupport().supported;
|
|
481
487
|
if (!isTTY && !opts.yes) {
|
|
482
488
|
process.stderr.write('Error: apply push requires --yes in non-interactive mode\n');
|
|
483
|
-
|
|
489
|
+
await exitWithAnalytics(1);
|
|
484
490
|
}
|
|
485
491
|
let inputs;
|
|
486
492
|
try {
|
|
@@ -488,17 +494,23 @@ export function registerApplyCommand(program) {
|
|
|
488
494
|
}
|
|
489
495
|
catch (e) {
|
|
490
496
|
if (e instanceof ApiError)
|
|
491
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
497
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
492
498
|
throw e;
|
|
493
499
|
}
|
|
494
500
|
const { components, tokens, client } = inputs;
|
|
495
|
-
|
|
501
|
+
const spaceId = opts.spaceId;
|
|
502
|
+
const environmentId = opts.environmentId;
|
|
503
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
504
|
+
space_key: spaceId,
|
|
505
|
+
environment_key: environmentId,
|
|
506
|
+
});
|
|
507
|
+
await assertNoSlotCycles(components);
|
|
496
508
|
try {
|
|
497
509
|
await client.validateToken();
|
|
498
510
|
}
|
|
499
511
|
catch (e) {
|
|
500
512
|
if (e instanceof ApiError)
|
|
501
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
513
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
502
514
|
throw e;
|
|
503
515
|
}
|
|
504
516
|
const manifest = buildManifest(components, tokens);
|
|
@@ -508,11 +520,10 @@ export function registerApplyCommand(program) {
|
|
|
508
520
|
}
|
|
509
521
|
catch (e) {
|
|
510
522
|
if (e instanceof ApiError)
|
|
511
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
523
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
512
524
|
throw e;
|
|
513
525
|
}
|
|
514
|
-
|
|
515
|
-
const environmentId = opts.environmentId;
|
|
526
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
516
527
|
if (opts.dryRun) {
|
|
517
528
|
if (isTTY) {
|
|
518
529
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
@@ -526,7 +537,7 @@ export function registerApplyCommand(program) {
|
|
|
526
537
|
else {
|
|
527
538
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
528
539
|
}
|
|
529
|
-
|
|
540
|
+
await exitWithAnalytics(0);
|
|
530
541
|
}
|
|
531
542
|
if (isEmptyPreview(preview)) {
|
|
532
543
|
if (isTTY && !opts.yes) {
|
|
@@ -535,14 +546,14 @@ export function registerApplyCommand(program) {
|
|
|
535
546
|
else {
|
|
536
547
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
537
548
|
}
|
|
538
|
-
|
|
549
|
+
await exitWithAnalytics(0);
|
|
539
550
|
}
|
|
540
551
|
const breakingWithImpact = hasBreakingChangesWithImpact(preview);
|
|
541
552
|
if (!isTTY || opts.yes) {
|
|
542
553
|
if (breakingWithImpact && !opts.force) {
|
|
543
554
|
process.stderr.write('Error: breaking changes with downstream impact detected. Use --force to acknowledge.\n');
|
|
544
555
|
process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
|
|
545
|
-
|
|
556
|
+
await exitWithAnalytics(1);
|
|
546
557
|
}
|
|
547
558
|
const verbose = opts.verbose ?? false;
|
|
548
559
|
if (verbose) {
|
|
@@ -557,7 +568,7 @@ export function registerApplyCommand(program) {
|
|
|
557
568
|
}
|
|
558
569
|
catch (e) {
|
|
559
570
|
if (e instanceof ApiError)
|
|
560
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
571
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
561
572
|
throw e;
|
|
562
573
|
}
|
|
563
574
|
process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
|
|
@@ -566,12 +577,14 @@ export function registerApplyCommand(program) {
|
|
|
566
577
|
}
|
|
567
578
|
catch (e) {
|
|
568
579
|
if (e instanceof ApiError)
|
|
569
|
-
die(`Error: ${formatApiError(e, opts.verbose)}
|
|
580
|
+
return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
|
|
570
581
|
throw e;
|
|
571
582
|
}
|
|
572
583
|
const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
|
|
584
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
573
585
|
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
574
|
-
|
|
586
|
+
const exitCode = operation.sys.status === 'succeeded' ? 0 : 1;
|
|
587
|
+
await exitWithAnalytics(exitCode);
|
|
575
588
|
return;
|
|
576
589
|
}
|
|
577
590
|
await new Promise((resolvePromise) => {
|
|
@@ -621,6 +634,7 @@ export function registerApplyCommand(program) {
|
|
|
621
634
|
}
|
|
622
635
|
throw e;
|
|
623
636
|
}
|
|
637
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
624
638
|
instance.rerender(createElement(ServerApplyDone, {
|
|
625
639
|
operation,
|
|
626
640
|
spaceId,
|
|
@@ -639,7 +653,7 @@ export function registerApplyCommand(program) {
|
|
|
639
653
|
void runApply(acknowledge, applyDeletions);
|
|
640
654
|
},
|
|
641
655
|
onCancel: () => {
|
|
642
|
-
|
|
656
|
+
void exitWithAnalytics(0);
|
|
643
657
|
},
|
|
644
658
|
}));
|
|
645
659
|
void instance.waitUntilExit().then(() => resolvePromise());
|
|
@@ -664,17 +678,17 @@ export function registerApplyCommand(program) {
|
|
|
664
678
|
}
|
|
665
679
|
catch (e) {
|
|
666
680
|
if (e instanceof ApiError)
|
|
667
|
-
die(`Error: ${formatApiError(e)}
|
|
681
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
668
682
|
throw e;
|
|
669
683
|
}
|
|
670
684
|
const { components, tokens, client } = inputs;
|
|
671
|
-
assertNoSlotCycles(components);
|
|
685
|
+
await assertNoSlotCycles(components);
|
|
672
686
|
try {
|
|
673
687
|
await client.validateToken();
|
|
674
688
|
}
|
|
675
689
|
catch (e) {
|
|
676
690
|
if (e instanceof ApiError)
|
|
677
|
-
die(`Error: ${formatApiError(e)}
|
|
691
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
678
692
|
throw e;
|
|
679
693
|
}
|
|
680
694
|
const fullManifest = buildManifest(components, tokens);
|
|
@@ -684,21 +698,26 @@ export function registerApplyCommand(program) {
|
|
|
684
698
|
}
|
|
685
699
|
catch (e) {
|
|
686
700
|
if (e instanceof ApiError)
|
|
687
|
-
die(`Error: ${formatApiError(e)}
|
|
701
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
688
702
|
throw e;
|
|
689
703
|
}
|
|
690
704
|
const spaceId = opts.spaceId;
|
|
691
705
|
const environmentId = opts.environmentId;
|
|
706
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
707
|
+
space_key: spaceId,
|
|
708
|
+
environment_key: environmentId,
|
|
709
|
+
});
|
|
710
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
692
711
|
const entities = getSelectableEntities(preview);
|
|
693
712
|
if (entities.length === 0) {
|
|
694
713
|
process.stderr.write('Nothing to change — design system is up to date.\n');
|
|
695
|
-
|
|
714
|
+
await exitWithAnalytics(0);
|
|
696
715
|
}
|
|
697
716
|
if (nonInteractive) {
|
|
698
717
|
const selectedKeys = resolveNonInteractiveSelection(entities, opts);
|
|
699
718
|
if (selectedKeys.size === 0) {
|
|
700
719
|
process.stderr.write('No entities matched selection criteria.\n');
|
|
701
|
-
|
|
720
|
+
await exitWithAnalytics(0);
|
|
702
721
|
}
|
|
703
722
|
const selectedComponentKeys = new Set();
|
|
704
723
|
const selectedTokenPaths = new Set();
|
|
@@ -714,7 +733,7 @@ export function registerApplyCommand(program) {
|
|
|
714
733
|
const hasBreaking = entities.some((e) => e.isBreaking && selectedKeys.has(makeSelectKey(e.kind, e.id)));
|
|
715
734
|
if (hasBreaking && !opts.force) {
|
|
716
735
|
process.stderr.write('Error: selection includes breaking changes. Use --force to acknowledge.\n');
|
|
717
|
-
|
|
736
|
+
await exitWithAnalytics(1);
|
|
718
737
|
}
|
|
719
738
|
let operation;
|
|
720
739
|
try {
|
|
@@ -725,7 +744,7 @@ export function registerApplyCommand(program) {
|
|
|
725
744
|
}
|
|
726
745
|
catch (e) {
|
|
727
746
|
if (e instanceof ApiError)
|
|
728
|
-
die(`Error: ${formatApiError(e)}
|
|
747
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
729
748
|
throw e;
|
|
730
749
|
}
|
|
731
750
|
process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
|
|
@@ -734,12 +753,13 @@ export function registerApplyCommand(program) {
|
|
|
734
753
|
}
|
|
735
754
|
catch (e) {
|
|
736
755
|
if (e instanceof ApiError)
|
|
737
|
-
die(`Error: ${formatApiError(e)}
|
|
756
|
+
return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
|
|
738
757
|
throw e;
|
|
739
758
|
}
|
|
740
759
|
const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
|
|
760
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
741
761
|
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
742
|
-
|
|
762
|
+
await exitWithAnalytics(operation.sys.status === 'succeeded' ? 0 : 1);
|
|
743
763
|
return;
|
|
744
764
|
}
|
|
745
765
|
await new Promise((resolvePromise) => {
|
|
@@ -801,6 +821,7 @@ export function registerApplyCommand(program) {
|
|
|
801
821
|
}
|
|
802
822
|
throw e;
|
|
803
823
|
}
|
|
824
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
804
825
|
instance.rerender(createElement(ServerApplyDone, {
|
|
805
826
|
operation,
|
|
806
827
|
spaceId,
|
|
@@ -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`)
|