@contentful/experience-design-system-cli 2.16.1-dev-build-2b79e46.0 → 2.16.1-dev-build-d068963.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 +3 -1
- package/dist/package.json +1 -1
- package/dist/src/analyze/command.js +2 -1
- package/dist/src/analyze/select/command.js +4 -4
- package/dist/src/apply/api-client.d.ts +10 -1
- package/dist/src/apply/api-client.js +122 -10
- package/dist/src/apply/command.js +7 -4
- package/dist/src/import/command.js +13 -8
- package/dist/src/import/tui/WizardApp.js +4 -12
- package/dist/src/lib/error-parser.d.ts +1 -0
- package/dist/src/lib/error-parser.js +2 -3
- package/dist/src/lib/terminal-capabilities.d.ts +28 -0
- package/dist/src/lib/terminal-capabilities.js +51 -0
- package/dist/src/print/command.js +2 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -444,7 +444,9 @@ Wizard run history is separate: `~/.config/experiences/runs.json`.
|
|
|
444
444
|
- 80+ columns recommended for full sidebar + detail view
|
|
445
445
|
- 120+ columns required to show the source code panel in `analyze select`
|
|
446
446
|
- `NO_COLOR=1` suppresses all ANSI color output
|
|
447
|
-
-
|
|
447
|
+
- Interactive views require both stdin and stdout to be TTYs and stdin to support raw mode. Read-only views fall back to plain or JSON output when those capabilities are unavailable; commands that require input stop with the relevant non-interactive flags in the error message.
|
|
448
|
+
- On Windows, use Windows Terminal with PowerShell. Older ConEmu and cmd.exe hosts may not provide the raw-mode support the interactive UI needs.
|
|
449
|
+
- To avoid the interactive UI, use the command's non-interactive options: `import --yes` (with credentials) or `import --no-push --auto-accept-scope`, `analyze select --select-all`, `apply select --select-all`, and `apply push --yes`.
|
|
448
450
|
|
|
449
451
|
---
|
|
450
452
|
|
package/dist/package.json
CHANGED
|
@@ -22,6 +22,7 @@ import { parsePromptOverrides, resolvePromptOverride } from '../lib/prompt-overr
|
|
|
22
22
|
import { runAgent } from '../generate/agent-runner.js';
|
|
23
23
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
24
24
|
import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
|
|
25
|
+
import { getInteractiveTerminalSupport } from '../lib/terminal-capabilities.js';
|
|
25
26
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
|
|
26
27
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
27
28
|
'.git',
|
|
@@ -505,7 +506,7 @@ export function registerAnalyzeCommand(program) {
|
|
|
505
506
|
totalErrors,
|
|
506
507
|
globalWarnings,
|
|
507
508
|
};
|
|
508
|
-
if (
|
|
509
|
+
if (getInteractiveTerminalSupport().supported) {
|
|
509
510
|
const { waitUntilExit } = render(createElement(AnalyzeView, {
|
|
510
511
|
result: analyzeResult,
|
|
511
512
|
onExit: () => process.exit(0),
|
|
@@ -2,6 +2,7 @@ import { access, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { createElement } from 'react';
|
|
4
4
|
import { render } from 'ink';
|
|
5
|
+
import { requireInteractiveTerminal } from '../../lib/terminal-capabilities.js';
|
|
5
6
|
import { getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, saveReviewState } from './persistence.js';
|
|
6
7
|
import { loadReviewInput } from './parser.js';
|
|
7
8
|
import { App } from './tui/App.js';
|
|
@@ -459,10 +460,9 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
459
460
|
`current-review-state.json=${paths.statePath}\n`);
|
|
460
461
|
return;
|
|
461
462
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
463
|
+
requireInteractiveTerminal({
|
|
464
|
+
alternative: 'pass `--accept-all`, `--select-all`, `--reject`, `--deselect`, `--select`, or `--patch`',
|
|
465
|
+
});
|
|
466
466
|
if (process.stdout.columns !== undefined && process.stdout.columns < 60) {
|
|
467
467
|
process.stderr.write(`Error: terminal too narrow (${process.stdout.columns} cols). Resize to 60+ columns.\n`);
|
|
468
468
|
process.exit(1);
|
|
@@ -8,11 +8,18 @@ export interface ApiClientOptions {
|
|
|
8
8
|
cmaToken: string;
|
|
9
9
|
spaceId: string;
|
|
10
10
|
environmentId: string;
|
|
11
|
+
retry?: {
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
initialDelayMs?: number;
|
|
14
|
+
maxDelayMs?: number;
|
|
15
|
+
sleep?: (delayMs: number) => Promise<void>;
|
|
16
|
+
};
|
|
11
17
|
}
|
|
12
18
|
export declare class ApiError extends Error {
|
|
13
19
|
readonly status: number;
|
|
14
20
|
readonly body: string;
|
|
15
|
-
|
|
21
|
+
readonly guidance?: string | undefined;
|
|
22
|
+
constructor(message: string, status: number, body: string, guidance?: string | undefined);
|
|
16
23
|
}
|
|
17
24
|
export interface PreviewValidationError {
|
|
18
25
|
componentName: string;
|
|
@@ -37,9 +44,11 @@ export declare class ImportApiClient {
|
|
|
37
44
|
private token;
|
|
38
45
|
private spaceId;
|
|
39
46
|
private environmentId;
|
|
47
|
+
private retry;
|
|
40
48
|
constructor(opts: ApiClientOptions);
|
|
41
49
|
private base;
|
|
42
50
|
private headers;
|
|
51
|
+
private fetchWithRetry;
|
|
43
52
|
validateToken(): Promise<void>;
|
|
44
53
|
previewImport(manifest: ManifestPayload): Promise<ServerPreviewResponse>;
|
|
45
54
|
applyImport(manifest: ManifestPayload, acknowledgeBreakingChanges: boolean): Promise<ApplyOperationResponse>;
|
|
@@ -12,6 +12,26 @@ export const APPLY_ERROR_PREFIX = 'apply failed:';
|
|
|
12
12
|
// to the prefixes as a deliberate, named contract rather than an inline
|
|
13
13
|
// magic string in the orchestrator.
|
|
14
14
|
export const VALIDATION_FAILED_CODE = '"ValidationFailed"';
|
|
15
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
16
|
+
function defaultSleep(delayMs) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
18
|
+
}
|
|
19
|
+
function isTransientStatus(status) {
|
|
20
|
+
return status >= 500 && status <= 599;
|
|
21
|
+
}
|
|
22
|
+
function retryAfterMs(response) {
|
|
23
|
+
const value = response.headers?.get('retry-after')?.trim();
|
|
24
|
+
if (!value)
|
|
25
|
+
return undefined;
|
|
26
|
+
const seconds = Number(value);
|
|
27
|
+
const delayMs = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(value) - Date.now();
|
|
28
|
+
if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > MAX_RETRY_AFTER_MS)
|
|
29
|
+
return undefined;
|
|
30
|
+
return Math.round(delayMs);
|
|
31
|
+
}
|
|
32
|
+
function errorMessage(error) {
|
|
33
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
34
|
+
}
|
|
15
35
|
// Cap on the body slice appended to ApiError.message. Bumped from 1000 →
|
|
16
36
|
// 16384 so realistic 422 ValidationFailed reports (which list every
|
|
17
37
|
// offending component, ~100 chars per error, easily exceeds 1KB once you
|
|
@@ -24,10 +44,12 @@ const ERROR_BODY_LOG_CAP = 16384;
|
|
|
24
44
|
export class ApiError extends Error {
|
|
25
45
|
status;
|
|
26
46
|
body;
|
|
27
|
-
|
|
47
|
+
guidance;
|
|
48
|
+
constructor(message, status, body, guidance) {
|
|
28
49
|
super(message);
|
|
29
50
|
this.status = status;
|
|
30
51
|
this.body = body;
|
|
52
|
+
this.guidance = guidance;
|
|
31
53
|
if (body) {
|
|
32
54
|
const trimmed = body.length > ERROR_BODY_LOG_CAP ? body.slice(0, ERROR_BODY_LOG_CAP) + '…' : body;
|
|
33
55
|
this.message = `${message}\n${trimmed}`;
|
|
@@ -136,11 +158,19 @@ export class ImportApiClient {
|
|
|
136
158
|
token;
|
|
137
159
|
spaceId;
|
|
138
160
|
environmentId;
|
|
161
|
+
retry;
|
|
139
162
|
constructor(opts) {
|
|
140
163
|
this.host = toApiHost(opts.host);
|
|
141
164
|
this.token = opts.cmaToken;
|
|
142
165
|
this.spaceId = opts.spaceId;
|
|
143
166
|
this.environmentId = opts.environmentId;
|
|
167
|
+
const initialDelayMs = Math.max(0, opts.retry?.initialDelayMs ?? 250);
|
|
168
|
+
this.retry = {
|
|
169
|
+
maxAttempts: Math.max(1, Math.floor(opts.retry?.maxAttempts ?? 3)),
|
|
170
|
+
initialDelayMs,
|
|
171
|
+
maxDelayMs: Math.max(initialDelayMs, opts.retry?.maxDelayMs ?? 2000),
|
|
172
|
+
sleep: opts.retry?.sleep ?? defaultSleep,
|
|
173
|
+
};
|
|
144
174
|
}
|
|
145
175
|
base() {
|
|
146
176
|
return `${this.host}/spaces/${this.spaceId}/environments/${this.environmentId}`;
|
|
@@ -152,6 +182,72 @@ export class ImportApiClient {
|
|
|
152
182
|
'X-Contentful-User-Agent': buildUserAgent(),
|
|
153
183
|
};
|
|
154
184
|
}
|
|
185
|
+
async fetchWithRetry(phase, url, init, context = {}) {
|
|
186
|
+
const debug = getDebugLogger();
|
|
187
|
+
const errorPrefix = phase === 'preview' ? PREVIEW_ERROR_PREFIX : 'poll failed:';
|
|
188
|
+
const startedAt = Date.now();
|
|
189
|
+
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
190
|
+
try {
|
|
191
|
+
const response = await fetch(url, init);
|
|
192
|
+
if (!isTransientStatus(response.status))
|
|
193
|
+
return response;
|
|
194
|
+
const body = await response.text();
|
|
195
|
+
if (attempt === this.retry.maxAttempts) {
|
|
196
|
+
const guidance = `The ${phase} request failed after ${attempt} attempts because the service remained unavailable. ` +
|
|
197
|
+
'Wait a moment and try again.';
|
|
198
|
+
debug.event('apply', `${phase}.error`, {
|
|
199
|
+
...context,
|
|
200
|
+
attempt,
|
|
201
|
+
maxAttempts: this.retry.maxAttempts,
|
|
202
|
+
status: response.status,
|
|
203
|
+
reason: 'retry_exhausted',
|
|
204
|
+
durationMs: Date.now() - startedAt,
|
|
205
|
+
bodyHead: body.slice(0, 2000),
|
|
206
|
+
});
|
|
207
|
+
throw new ApiError(`${errorPrefix} ${response.status}`, response.status, body, guidance);
|
|
208
|
+
}
|
|
209
|
+
const backoffMs = Math.min(this.retry.initialDelayMs * 2 ** (attempt - 1), this.retry.maxDelayMs);
|
|
210
|
+
const delayMs = retryAfterMs(response) ?? backoffMs;
|
|
211
|
+
debug.event('apply', `${phase}.retry`, {
|
|
212
|
+
...context,
|
|
213
|
+
attempt,
|
|
214
|
+
maxAttempts: this.retry.maxAttempts,
|
|
215
|
+
status: response.status,
|
|
216
|
+
reason: 'http_5xx',
|
|
217
|
+
delayMs,
|
|
218
|
+
});
|
|
219
|
+
await this.retry.sleep(delayMs);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof ApiError)
|
|
223
|
+
throw error;
|
|
224
|
+
if (attempt === this.retry.maxAttempts) {
|
|
225
|
+
const body = `The request failed after ${attempt} attempts because of a network error: ${errorMessage(error)}. ` +
|
|
226
|
+
'Check your connection and try again.';
|
|
227
|
+
debug.event('apply', `${phase}.error`, {
|
|
228
|
+
...context,
|
|
229
|
+
attempt,
|
|
230
|
+
maxAttempts: this.retry.maxAttempts,
|
|
231
|
+
status: 0,
|
|
232
|
+
reason: 'transport_retry_exhausted',
|
|
233
|
+
durationMs: Date.now() - startedAt,
|
|
234
|
+
});
|
|
235
|
+
throw new ApiError(`${errorPrefix} 0`, 0, body);
|
|
236
|
+
}
|
|
237
|
+
const delayMs = Math.min(this.retry.initialDelayMs * 2 ** (attempt - 1), this.retry.maxDelayMs);
|
|
238
|
+
debug.event('apply', `${phase}.retry`, {
|
|
239
|
+
...context,
|
|
240
|
+
attempt,
|
|
241
|
+
maxAttempts: this.retry.maxAttempts,
|
|
242
|
+
status: 0,
|
|
243
|
+
reason: 'transport_error',
|
|
244
|
+
delayMs,
|
|
245
|
+
});
|
|
246
|
+
await this.retry.sleep(delayMs);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new Error('Retry attempts exhausted');
|
|
250
|
+
}
|
|
155
251
|
async validateToken() {
|
|
156
252
|
// /users/me is the canonical token-validity endpoint — avoids space-membership
|
|
157
253
|
// false positives that don't apply to the design-systems API authorization path.
|
|
@@ -173,7 +269,7 @@ export class ImportApiClient {
|
|
|
173
269
|
componentCount: manifest.components?.length ?? 0,
|
|
174
270
|
tokenCount: manifest.designTokens?.length ?? 0,
|
|
175
271
|
});
|
|
176
|
-
const res = await
|
|
272
|
+
const res = await this.fetchWithRetry('preview', url, {
|
|
177
273
|
method: 'POST',
|
|
178
274
|
headers: this.headers(),
|
|
179
275
|
body: JSON.stringify(manifest),
|
|
@@ -196,19 +292,35 @@ export class ImportApiClient {
|
|
|
196
292
|
const debug = getDebugLogger();
|
|
197
293
|
const startedAt = Date.now();
|
|
198
294
|
debug.event('apply', 'apply.request', { url, acknowledgeBreakingChanges });
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
295
|
+
let res;
|
|
296
|
+
try {
|
|
297
|
+
res = await fetch(url, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: this.headers(),
|
|
300
|
+
body: JSON.stringify({ ...manifest, acknowledgeBreakingChanges }),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
const body = `The apply request was not retried because its outcome is unknown and retrying could start a duplicate operation. ` +
|
|
305
|
+
`Check for an existing operation before trying again. Cause: ${errorMessage(error)}`;
|
|
306
|
+
debug.event('apply', 'apply.error', {
|
|
307
|
+
status: 0,
|
|
308
|
+
durationMs: Date.now() - startedAt,
|
|
309
|
+
reason: 'transport_error_not_retried',
|
|
310
|
+
});
|
|
311
|
+
throw new ApiError(`${APPLY_ERROR_PREFIX} 0`, 0, body);
|
|
312
|
+
}
|
|
204
313
|
if (!res.ok) {
|
|
205
314
|
const body = await res.text();
|
|
315
|
+
const guidance = isTransientStatus(res.status)
|
|
316
|
+
? 'The apply request was not retried because the server may already have started an operation and retrying could create a duplicate. Check for an existing operation before trying again.'
|
|
317
|
+
: undefined;
|
|
206
318
|
debug.event('apply', 'apply.error', {
|
|
207
319
|
status: res.status,
|
|
208
320
|
durationMs: Date.now() - startedAt,
|
|
209
321
|
bodyHead: body.slice(0, 2000),
|
|
210
322
|
});
|
|
211
|
-
throw new ApiError(`${APPLY_ERROR_PREFIX} ${res.status}`, res.status, body);
|
|
323
|
+
throw new ApiError(`${APPLY_ERROR_PREFIX} ${res.status}`, res.status, body, guidance);
|
|
212
324
|
}
|
|
213
325
|
const parsed = (await res.json());
|
|
214
326
|
debug.event('apply', 'apply.accepted', {
|
|
@@ -225,10 +337,10 @@ export class ImportApiClient {
|
|
|
225
337
|
const terminalStatuses = new Set(['succeeded', 'partial', 'failed']);
|
|
226
338
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
227
339
|
const url = `${this.base()}/design_systems/imports/apply/${encodeURIComponent(operationId)}`;
|
|
228
|
-
const res = await
|
|
340
|
+
const res = await this.fetchWithRetry('poll', url, {
|
|
229
341
|
method: 'GET',
|
|
230
342
|
headers: this.headers(),
|
|
231
|
-
});
|
|
343
|
+
}, { operationId });
|
|
232
344
|
if (!res.ok) {
|
|
233
345
|
throw new ApiError(`poll failed: ${res.status}`, res.status, await res.text());
|
|
234
346
|
}
|
|
@@ -14,6 +14,7 @@ import { buildPostPushUrl } from '../lib/contentful-urls.js';
|
|
|
14
14
|
import { resolveCompositionMode } from '../lib/composition-mode.js';
|
|
15
15
|
import { stripAllowedComponents } from '../import/strip-allowed-components.js';
|
|
16
16
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
17
|
+
import { getInteractiveTerminalSupport, requireInteractiveTerminal } from '../lib/terminal-capabilities.js';
|
|
17
18
|
function die(message) {
|
|
18
19
|
process.stderr.write(`${message}\n`);
|
|
19
20
|
process.exit(1);
|
|
@@ -461,7 +462,7 @@ export function registerApplyCommand(program) {
|
|
|
461
462
|
}
|
|
462
463
|
const spaceId = opts.spaceId;
|
|
463
464
|
const environmentId = opts.environmentId;
|
|
464
|
-
if (
|
|
465
|
+
if (getInteractiveTerminalSupport().supported) {
|
|
465
466
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
466
467
|
preview,
|
|
467
468
|
spaceId,
|
|
@@ -491,7 +492,7 @@ export function registerApplyCommand(program) {
|
|
|
491
492
|
.option('--force', 'Skip confirmation for breaking changes (for CI)')
|
|
492
493
|
.option('--dry-run', 'Run preview only without applying')
|
|
493
494
|
.action(async (opts) => {
|
|
494
|
-
const isTTY =
|
|
495
|
+
const isTTY = getInteractiveTerminalSupport().supported;
|
|
495
496
|
if (!isTTY && !opts.yes) {
|
|
496
497
|
process.stderr.write('Error: apply push requires --yes in non-interactive mode\n');
|
|
497
498
|
process.exit(1);
|
|
@@ -669,8 +670,10 @@ export function registerApplyCommand(program) {
|
|
|
669
670
|
.option('--force', 'Skip confirmation for breaking changes')
|
|
670
671
|
.action(async (opts) => {
|
|
671
672
|
const nonInteractive = opts.selectAll || (opts.select ?? []).length > 0 || (opts.deselect ?? []).length > 0;
|
|
672
|
-
if (!nonInteractive
|
|
673
|
-
|
|
673
|
+
if (!nonInteractive) {
|
|
674
|
+
requireInteractiveTerminal({
|
|
675
|
+
alternative: 'pass `--select-all`, `--select`, or `--deselect`',
|
|
676
|
+
});
|
|
674
677
|
}
|
|
675
678
|
let inputs;
|
|
676
679
|
try {
|
|
@@ -11,6 +11,7 @@ import { pickerPushRun } from '../runs/push-launcher.js';
|
|
|
11
11
|
import { resolvePromptFlags } from './print-prompt.js';
|
|
12
12
|
import { shouldShowRunPicker } from '../runs/run-picker-mount.js';
|
|
13
13
|
import { dispatchPickerSelection } from './picker-dispatch.js';
|
|
14
|
+
import { getInteractiveTerminalSupport, requireInteractiveTerminal } from '../lib/terminal-capabilities.js';
|
|
14
15
|
export function registerImportCommand(program) {
|
|
15
16
|
const cmd = program
|
|
16
17
|
.command('import')
|
|
@@ -72,6 +73,7 @@ export function registerImportCommand(program) {
|
|
|
72
73
|
.option('--save-as-new', 'Only valid with --modify: always save to a new path (prompts for one)')
|
|
73
74
|
.option('--force', 'Bypass staleness checks when paired with --push-from-run or --modify.')
|
|
74
75
|
.action(async (opts) => {
|
|
76
|
+
const interactiveTerminalSupported = getInteractiveTerminalSupport().supported;
|
|
75
77
|
// --modify and --push-from-run resume a recorded session; the composition
|
|
76
78
|
// mode comes from that run's record, so composition flags on the command
|
|
77
79
|
// line don't apply. Warn and clear them rather than let them mislead.
|
|
@@ -130,7 +132,7 @@ export function registerImportCommand(program) {
|
|
|
130
132
|
...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
|
|
131
133
|
...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
|
|
132
134
|
...(opts.host ? { host: opts.host } : {}),
|
|
133
|
-
interactive:
|
|
135
|
+
interactive: interactiveTerminalSupported,
|
|
134
136
|
...(opts.force ? { force: true } : {}),
|
|
135
137
|
});
|
|
136
138
|
return;
|
|
@@ -152,6 +154,9 @@ export function registerImportCommand(program) {
|
|
|
152
154
|
process.exit(1);
|
|
153
155
|
return;
|
|
154
156
|
}
|
|
157
|
+
requireInteractiveTerminal({
|
|
158
|
+
alternative: 'start a fresh headless import with `--yes` and the required credentials',
|
|
159
|
+
});
|
|
155
160
|
try {
|
|
156
161
|
await modifyRun({
|
|
157
162
|
runIdOrPath: opts.modify,
|
|
@@ -221,7 +226,7 @@ export function registerImportCommand(program) {
|
|
|
221
226
|
opts.skipGenerate ||
|
|
222
227
|
// A "don't push" request on a non-TTY is a headless intent (the wizard
|
|
223
228
|
// needs a TTY); in a TTY it stays interactive and is NOT headless.
|
|
224
|
-
(noPushRequested && !
|
|
229
|
+
(noPushRequested && !interactiveTerminalSupported) ||
|
|
225
230
|
!!opts.spaceId ||
|
|
226
231
|
!!opts.environmentId ||
|
|
227
232
|
!!opts.cmaToken ||
|
|
@@ -229,12 +234,12 @@ export function registerImportCommand(program) {
|
|
|
229
234
|
dryRunForward ||
|
|
230
235
|
false;
|
|
231
236
|
const autoAcceptScope = opts.autoAcceptScope ?? false;
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
237
|
+
if (!interactiveTerminalSupported && !isHeadless && !autoAcceptScope) {
|
|
238
|
+
requireInteractiveTerminal({
|
|
239
|
+
alternative: 'use headless flags such as `--yes` with credentials, or `--no-push --auto-accept-scope`',
|
|
240
|
+
});
|
|
236
241
|
}
|
|
237
|
-
if (
|
|
242
|
+
if (interactiveTerminalSupported && !isHeadless) {
|
|
238
243
|
const { render } = await import('ink');
|
|
239
244
|
const { createElement } = await import('react');
|
|
240
245
|
const { WizardApp } = await import('./tui/WizardApp.js');
|
|
@@ -300,7 +305,7 @@ export function registerImportCommand(program) {
|
|
|
300
305
|
...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
|
|
301
306
|
...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
|
|
302
307
|
...(opts.host ? { host: opts.host } : {}),
|
|
303
|
-
interactive:
|
|
308
|
+
interactive: true,
|
|
304
309
|
...(opts.outDir ? { outDir: opts.outDir } : {}),
|
|
305
310
|
...(opts.overwrite ? { overwrite: true } : {}),
|
|
306
311
|
...(opts.saveAsNew ? { saveAsNew: true } : {}),
|
|
@@ -37,7 +37,7 @@ import { ImportApiClient, ApiError } from '../../apply/api-client.js';
|
|
|
37
37
|
import { detectSlotCycles, extractComponentsFromManifest, formatSlotCycleReport } from '../../apply/command.js';
|
|
38
38
|
import { findSlotCycles } from '../../analyze/cycle-detection.js';
|
|
39
39
|
import { buildComponentGraph } from '../../analyze/slot-graph.js';
|
|
40
|
-
import {
|
|
40
|
+
import { formatApiError, formatEdsiError } from '../../lib/error-parser.js';
|
|
41
41
|
import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
|
|
42
42
|
import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
|
|
43
43
|
import { spawnGenerateChild } from './spawn-generate.js';
|
|
@@ -924,13 +924,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
924
924
|
});
|
|
925
925
|
return;
|
|
926
926
|
}
|
|
927
|
+
const formatted = formatApiError(e, process.env['EDSI_VERBOSE_ERRORS'] === '1');
|
|
927
928
|
update({
|
|
928
929
|
step: 'error',
|
|
929
930
|
errorStep: 'apply preview',
|
|
930
|
-
errorMessage:
|
|
931
|
-
verbose: process.env['EDSI_VERBOSE_ERRORS'] === '1',
|
|
932
|
-
raw: e.body,
|
|
933
|
-
}) || e.message,
|
|
931
|
+
errorMessage: formatted,
|
|
934
932
|
errorAllowCredentialRetry: true,
|
|
935
933
|
});
|
|
936
934
|
return;
|
|
@@ -1092,13 +1090,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
|
|
|
1092
1090
|
catch (e) {
|
|
1093
1091
|
let msg;
|
|
1094
1092
|
if (e instanceof ApiError) {
|
|
1095
|
-
|
|
1096
|
-
msg = formatParsedEdsiError(parsed, {
|
|
1097
|
-
verbose: process.env['EDSI_VERBOSE_ERRORS'] === '1',
|
|
1098
|
-
raw: e.body,
|
|
1099
|
-
});
|
|
1100
|
-
if (!msg)
|
|
1101
|
-
msg = e.message;
|
|
1093
|
+
msg = formatApiError(e, process.env['EDSI_VERBOSE_ERRORS'] === '1');
|
|
1102
1094
|
}
|
|
1103
1095
|
else if (e instanceof Error) {
|
|
1104
1096
|
msg = e.message;
|
|
@@ -258,7 +258,6 @@ export function formatEdsiError(raw, opts = {}) {
|
|
|
258
258
|
export function formatApiError(error, verbose = false) {
|
|
259
259
|
const formatted = formatEdsiError(error.body || error.message, { verbose, raw: error.body }) || error.message;
|
|
260
260
|
const phase = error.message.split('\n', 1)[0];
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
: formatted;
|
|
261
|
+
const withPhase = /^(?:apply|preview|poll) failed: \d+$/.test(phase) && formatted !== phase ? `${phase}\n${formatted}` : formatted;
|
|
262
|
+
return error.guidance ? `${withPhase}\n${error.guidance}` : withPhase;
|
|
264
263
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type TerminalInput = {
|
|
2
|
+
isTTY?: boolean;
|
|
3
|
+
isRaw?: boolean;
|
|
4
|
+
setRawMode?: (enabled: boolean) => unknown;
|
|
5
|
+
};
|
|
6
|
+
export type TerminalOutput = {
|
|
7
|
+
isTTY?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type InteractiveTerminalSupport = {
|
|
10
|
+
supported: true;
|
|
11
|
+
} | {
|
|
12
|
+
supported: false;
|
|
13
|
+
reason: 'input-not-tty' | 'output-not-tty' | 'raw-mode-unavailable';
|
|
14
|
+
};
|
|
15
|
+
export type TerminalCapabilityOptions = {
|
|
16
|
+
stdin?: TerminalInput;
|
|
17
|
+
stdout?: TerminalOutput;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Check the capabilities Ink's interactive input hooks require before the UI
|
|
21
|
+
* mounts. A stream can report itself as a TTY while its terminal host still
|
|
22
|
+
* rejects raw mode, so the check briefly enables and restores raw mode.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getInteractiveTerminalSupport(options?: TerminalCapabilityOptions): InteractiveTerminalSupport;
|
|
25
|
+
export type RequireInteractiveTerminalOptions = TerminalCapabilityOptions & {
|
|
26
|
+
alternative?: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function requireInteractiveTerminal(options?: RequireInteractiveTerminalOptions): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check the capabilities Ink's interactive input hooks require before the UI
|
|
3
|
+
* mounts. A stream can report itself as a TTY while its terminal host still
|
|
4
|
+
* rejects raw mode, so the check briefly enables and restores raw mode.
|
|
5
|
+
*/
|
|
6
|
+
export function getInteractiveTerminalSupport(options = {}) {
|
|
7
|
+
const stdin = options.stdin ?? process.stdin;
|
|
8
|
+
const stdout = options.stdout ?? process.stdout;
|
|
9
|
+
if (stdout.isTTY !== true)
|
|
10
|
+
return { supported: false, reason: 'output-not-tty' };
|
|
11
|
+
if (stdin.isTTY !== true)
|
|
12
|
+
return { supported: false, reason: 'input-not-tty' };
|
|
13
|
+
if (typeof stdin.setRawMode !== 'function') {
|
|
14
|
+
return { supported: false, reason: 'raw-mode-unavailable' };
|
|
15
|
+
}
|
|
16
|
+
const wasRaw = stdin.isRaw === true;
|
|
17
|
+
try {
|
|
18
|
+
stdin.setRawMode(true);
|
|
19
|
+
if (!wasRaw)
|
|
20
|
+
stdin.setRawMode(false);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
if (!wasRaw) {
|
|
24
|
+
try {
|
|
25
|
+
stdin.setRawMode(false);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// The original capability failure is the actionable result.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { supported: false, reason: 'raw-mode-unavailable' };
|
|
32
|
+
}
|
|
33
|
+
return { supported: true };
|
|
34
|
+
}
|
|
35
|
+
export function requireInteractiveTerminal(options = {}) {
|
|
36
|
+
const support = getInteractiveTerminalSupport(options);
|
|
37
|
+
if (support.supported)
|
|
38
|
+
return;
|
|
39
|
+
const reason = support.reason === 'output-not-tty'
|
|
40
|
+
? 'standard output is not a TTY'
|
|
41
|
+
: support.reason === 'input-not-tty'
|
|
42
|
+
? 'standard input is not a TTY'
|
|
43
|
+
: 'standard input cannot enter raw mode';
|
|
44
|
+
const lines = [
|
|
45
|
+
`Interactive terminal UI is unavailable because ${reason}.`,
|
|
46
|
+
'On Windows, run this command in Windows Terminal with PowerShell.',
|
|
47
|
+
];
|
|
48
|
+
if (options.alternative)
|
|
49
|
+
lines.push(`To continue without the UI, ${options.alternative}.`);
|
|
50
|
+
throw new Error(lines.join('\n'));
|
|
51
|
+
}
|
|
@@ -7,6 +7,7 @@ import { validateCDFFile } from './validate/validators/cdf-validator.js';
|
|
|
7
7
|
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
|
+
import { getInteractiveTerminalSupport } from '../lib/terminal-capabilities.js';
|
|
10
11
|
function die(message) {
|
|
11
12
|
process.stderr.write(`${message}\n`);
|
|
12
13
|
process.exit(1);
|
|
@@ -195,7 +196,7 @@ export function registerPrintCommand(program) {
|
|
|
195
196
|
}
|
|
196
197
|
const failed = viewResults.some((r) => !r.valid);
|
|
197
198
|
const exitCode = failed ? 1 : 0;
|
|
198
|
-
if (
|
|
199
|
+
if (getInteractiveTerminalSupport().supported) {
|
|
199
200
|
const { waitUntilExit } = render(createElement(ValidateView, {
|
|
200
201
|
results: viewResults,
|
|
201
202
|
onExit: () => process.exit(exitCode),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.16.1-dev-build-
|
|
3
|
+
"version": "2.16.1-dev-build-d068963.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"react": "^18.3.1",
|
|
35
35
|
"react-devtools-core": "^4.19.1",
|
|
36
36
|
"react-dom": "^18.3.1",
|
|
37
|
-
"@contentful/experience-design-system-extraction": "2.16.1-dev-build-
|
|
38
|
-
"@contentful/experience-design-system-types": "2.16.1-dev-build-
|
|
37
|
+
"@contentful/experience-design-system-extraction": "2.16.1-dev-build-d068963.0",
|
|
38
|
+
"@contentful/experience-design-system-types": "2.16.1-dev-build-d068963.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@tsconfig/node24": "^24.0.3",
|