@contentful/experience-design-system-cli 2.26.1-dev-build-64063df.0 → 2.26.1-dev-build-f1d1f30.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/dist/package.json +13 -9
- package/dist/src/index.js +34937 -9
- package/dist/src/lib/cli-path.d.ts +4 -0
- package/dist/src/runs/export-helpers.d.ts +1 -12
- package/dist/src/runs/push-helpers.d.ts +1 -6
- package/package.json +12 -7
- package/skills/generate-components.md +427 -0
- package/skills/generate-tokens.md +194 -0
- package/skills/select-components.md +224 -0
- package/dist/src/analytics/apply.js +0 -30
- package/dist/src/analytics/client.js +0 -81
- package/dist/src/analytics/constants.js +0 -4
- package/dist/src/analytics/env.js +0 -14
- package/dist/src/analytics/exit.js +0 -24
- package/dist/src/analytics/index.js +0 -9
- package/dist/src/analytics/normalize.js +0 -18
- package/dist/src/analytics/os.js +0 -13
- package/dist/src/analytics/session.js +0 -15
- package/dist/src/analytics/tracker.js +0 -126
- package/dist/src/analytics/types.js +0 -1
- package/dist/src/analyze/build-analyze-view-rows.js +0 -63
- package/dist/src/analyze/command.js +0 -599
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +0 -22
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +0 -44
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +0 -31
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +0 -88
- package/dist/src/analyze/composition/agent-parser/sandbox.js +0 -187
- package/dist/src/analyze/composition/apply-mapping.js +0 -67
- package/dist/src/analyze/composition/candidate-critic-agent.js +0 -39
- package/dist/src/analyze/composition/candidate-critic.js +0 -63
- package/dist/src/analyze/composition/candidate-files.js +0 -131
- package/dist/src/analyze/composition/composition-cache-key.js +0 -19
- package/dist/src/analyze/composition/interchange-schema.js +0 -61
- package/dist/src/analyze/composition/manifest-doc-evidence.js +0 -136
- package/dist/src/analyze/composition/merge-edges.js +0 -70
- package/dist/src/analyze/composition/parse-map-edges.js +0 -55
- package/dist/src/analyze/composition/resolve-mapping-cli.js +0 -29
- package/dist/src/analyze/composition/resolve-mapping.js +0 -111
- package/dist/src/analyze/cycle-detection.js +0 -234
- package/dist/src/analyze/cycle-view.js +0 -28
- package/dist/src/analyze/fuzzy-search.js +0 -55
- package/dist/src/analyze/issue-inheritance.js +0 -108
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.js +0 -170
- package/dist/src/analyze/search-neighborhood.js +0 -56
- package/dist/src/analyze/select/command.js +0 -484
- package/dist/src/analyze/select/index.js +0 -5
- package/dist/src/analyze/select/parser.js +0 -53
- package/dist/src/analyze/select/persistence.js +0 -80
- package/dist/src/analyze/select/preview-annotations.js +0 -62
- package/dist/src/analyze/select/stdout.js +0 -3
- package/dist/src/analyze/select/tui/App.js +0 -483
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +0 -48
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +0 -125
- package/dist/src/analyze/select/tui/components/FieldEditor.js +0 -1131
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +0 -36
- package/dist/src/analyze/select/tui/components/GotoBanner.js +0 -30
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +0 -517
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +0 -17
- package/dist/src/analyze/select/tui/components/JsonEditor.js +0 -131
- package/dist/src/analyze/select/tui/components/JsonPanel.js +0 -66
- package/dist/src/analyze/select/tui/components/LineagePanel.js +0 -22
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +0 -29
- package/dist/src/analyze/select/tui/components/QuitDialog.js +0 -14
- package/dist/src/analyze/select/tui/components/RationalePanel.js +0 -80
- package/dist/src/analyze/select/tui/components/Sidebar.js +0 -92
- package/dist/src/analyze/select/tui/components/SourcePanel.js +0 -52
- package/dist/src/analyze/select/tui/components/StatusBar.js +0 -7
- package/dist/src/analyze/select/tui/components/TopBar.js +0 -10
- package/dist/src/analyze/select/tui/components/removed-components-text.js +0 -11
- package/dist/src/analyze/select/tui/hooks/scroll-offset.js +0 -26
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +0 -66
- package/dist/src/analyze/select/tui/hooks/useKeymap.js +0 -67
- package/dist/src/analyze/select/tui/hooks/useSession.js +0 -52
- package/dist/src/analyze/select/tui/hooks/useUndo.js +0 -26
- package/dist/src/analyze/select/tui/theme.js +0 -28
- package/dist/src/analyze/select/types.js +0 -39
- package/dist/src/analyze/select-agent/command.js +0 -558
- package/dist/src/analyze/select-agent/context-builder.js +0 -197
- package/dist/src/analyze/select-agent/show-rationale.js +0 -92
- package/dist/src/analyze/selection-cascade.js +0 -19
- package/dist/src/analyze/slot-graph.js +0 -17
- package/dist/src/analyze/tui/AnalyzeView.js +0 -64
- package/dist/src/apply/api-client.js +0 -440
- package/dist/src/apply/command.js +0 -845
- package/dist/src/apply/manifest.js +0 -1
- package/dist/src/apply/preview-utils.js +0 -26
- package/dist/src/apply/tui/SelectView.js +0 -34
- package/dist/src/apply/tui/ServerApplyView.js +0 -49
- package/dist/src/apply/tui/ServerPreviewView.js +0 -23
- package/dist/src/credentials-store.js +0 -74
- package/dist/src/generate/command.js +0 -597
- package/dist/src/generate/edit/command.js +0 -127
- package/dist/src/generate/tui/GenerateView.js +0 -10
- package/dist/src/host-utils.js +0 -26
- package/dist/src/import/agent-model-resolve.js +0 -35
- package/dist/src/import/auto-filter-resolve.js +0 -16
- package/dist/src/import/command.js +0 -380
- package/dist/src/import/cycle-auto-reject.js +0 -14
- package/dist/src/import/orchestrator.js +0 -603
- package/dist/src/import/path-utils.js +0 -30
- package/dist/src/import/picker-dispatch.js +0 -44
- package/dist/src/import/print-prompt.js +0 -31
- package/dist/src/import/strip-allowed-components.js +0 -22
- package/dist/src/import/tui/CustomPromptBanner.js +0 -8
- package/dist/src/import/tui/WizardApp.js +0 -1670
- package/dist/src/import/tui/ai-flag.js +0 -6
- package/dist/src/import/tui/auto-filter-error.js +0 -29
- package/dist/src/import/tui/autocomplete.js +0 -32
- package/dist/src/import/tui/components/AutoFilterBanner.js +0 -19
- package/dist/src/import/tui/components/CounterStrip.js +0 -12
- package/dist/src/import/tui/components/LegendEntry.js +0 -6
- package/dist/src/import/tui/cycle-panel-scroll.js +0 -33
- package/dist/src/import/tui/final-review-host.js +0 -25
- package/dist/src/import/tui/group-collapse.js +0 -11
- package/dist/src/import/tui/history.js +0 -53
- package/dist/src/import/tui/hooks/useLineage.js +0 -50
- package/dist/src/import/tui/hooks/useOverlayPanel.js +0 -27
- package/dist/src/import/tui/lineage-layout.js +0 -82
- package/dist/src/import/tui/merge-ai-decisions.js +0 -22
- package/dist/src/import/tui/push-decision-gate-helpers.js +0 -8
- package/dist/src/import/tui/push-progress.js +0 -1
- package/dist/src/import/tui/run-print-files-helpers.js +0 -8
- package/dist/src/import/tui/run-teaser.js +0 -9
- package/dist/src/import/tui/runLivePreview.js +0 -82
- package/dist/src/import/tui/runScopeGate.js +0 -28
- package/dist/src/import/tui/scope-gate-columns.js +0 -94
- package/dist/src/import/tui/scope-gate-host.js +0 -28
- package/dist/src/import/tui/sidebar-width.js +0 -3
- package/dist/src/import/tui/spawn-generate.js +0 -53
- package/dist/src/import/tui/step-filters.js +0 -42
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +0 -726
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +0 -210
- package/dist/src/import/tui/steps/CredentialsStep.js +0 -108
- package/dist/src/import/tui/steps/DoneStep.js +0 -24
- package/dist/src/import/tui/steps/ErrorStep.js +0 -16
- package/dist/src/import/tui/steps/GateStep.js +0 -22
- package/dist/src/import/tui/steps/GenerateReviewStep.js +0 -1618
- package/dist/src/import/tui/steps/PathValidationStep.js +0 -152
- package/dist/src/import/tui/steps/PreviewStep.js +0 -37
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +0 -23
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +0 -73
- package/dist/src/import/tui/steps/PushingStep.js +0 -24
- package/dist/src/import/tui/steps/RunningStep.js +0 -21
- package/dist/src/import/tui/steps/ScopeGateStep.js +0 -907
- package/dist/src/import/tui/steps/TokenInputStep.js +0 -88
- package/dist/src/import/tui/steps/WelcomeStep.js +0 -34
- package/dist/src/import/tui/steps/WizardPreviewStep.js +0 -169
- package/dist/src/import/tui/steps/auto-reject-decision.js +0 -9
- package/dist/src/import/tui/steps/breaking-change-format.js +0 -24
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +0 -175
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.js +0 -122
- package/dist/src/import/tui/wizard-422-helpers.js +0 -60
- package/dist/src/import/tui/wizard-generate-progress.js +0 -25
- package/dist/src/import/tui/wizard-save-flow.js +0 -20
- package/dist/src/import/tui/wizard-state-transitions.js +0 -32
- package/dist/src/lib/agent-model-options.js +0 -15
- package/dist/src/lib/command-options.js +0 -30
- package/dist/src/lib/composition-mode.js +0 -39
- package/dist/src/lib/contentful-urls.js +0 -27
- package/dist/src/lib/debug-logger.js +0 -229
- package/dist/src/lib/debug-preamble.js +0 -33
- package/dist/src/lib/error-parser.js +0 -269
- package/dist/src/lib/prompt-overrides.js +0 -69
- package/dist/src/lib/terminal-capabilities.js +0 -51
- package/dist/src/lib/user-agent.js +0 -29
- package/dist/src/output/format.js +0 -110
- package/dist/src/print/command.js +0 -221
- package/dist/src/print/validate/tui/ValidateView.js +0 -37
- package/dist/src/print/validate/validators/cdf-validator.js +0 -104
- package/dist/src/print/validate/validators/dtcg-validator.js +0 -110
- package/dist/src/print/validate/validators/format-errors.js +0 -18
- package/dist/src/program.js +0 -105
- package/dist/src/runs/export-helpers.js +0 -59
- package/dist/src/runs/fingerprint.js +0 -75
- package/dist/src/runs/ls-command.js +0 -137
- package/dist/src/runs/modify-launcher.js +0 -42
- package/dist/src/runs/path-prompt.js +0 -72
- package/dist/src/runs/push-creds-prompt.js +0 -39
- package/dist/src/runs/push-helpers.js +0 -72
- package/dist/src/runs/push-launcher.js +0 -77
- package/dist/src/runs/replay-helpers.js +0 -164
- package/dist/src/runs/resolve-run-target.js +0 -45
- package/dist/src/runs/run-picker-mount.js +0 -72
- package/dist/src/runs/run-picker.js +0 -129
- package/dist/src/runs/save-conflict.js +0 -56
- package/dist/src/runs/save-path-resolver.js +0 -75
- package/dist/src/runs/staleness.js +0 -140
- package/dist/src/runs/store.js +0 -136
- package/dist/src/session/cache-keys.js +0 -44
- package/dist/src/session/command.js +0 -261
- package/dist/src/session/db.js +0 -1538
- package/dist/src/session/migration.js +0 -117
- package/dist/src/session/session-id.js +0 -212
- package/dist/src/session/stats.js +0 -89
- package/dist/src/setup/analytics-prompt.js +0 -24
- package/dist/src/setup/auto-filter-prompt.js +0 -24
- package/dist/src/setup/command.js +0 -966
- package/dist/src/setup/composition-mode-prompt.js +0 -37
- package/dist/src/setup/debug-mode-prompt.js +0 -22
- package/dist/src/types.js +0 -1
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
import { designSystemImportSourcelessPreview, designSystemImportApply, designSystemImportGetOperation, } from '@contentful/experience-design-system-client';
|
|
2
|
-
import { DEFAULT_API_HOST, toApiHost } from '../host-utils.js';
|
|
3
|
-
import { getDebugLogger } from '../lib/debug-logger.js';
|
|
4
|
-
import { buildUserAgent } from '../lib/user-agent.js';
|
|
5
|
-
export const DEFAULT_HOST = DEFAULT_API_HOST;
|
|
6
|
-
export const PREVIEW_ERROR_PREFIX = 'preview failed:';
|
|
7
|
-
export const APPLY_ERROR_PREFIX = 'apply failed:';
|
|
8
|
-
// Substring match the orchestrator uses to distinguish a parseable
|
|
9
|
-
// component-level validation failure from generic 422s. Quoted because the
|
|
10
|
-
// match runs against the raw JSON body (which contains `"code":"ValidationFailed"`).
|
|
11
|
-
// If the server ever changes the casing or naming, isPreviewValidationError
|
|
12
|
-
// silently returns false and the retry loop never fires — so this lives next
|
|
13
|
-
// to the prefixes as a deliberate, named contract rather than an inline
|
|
14
|
-
// magic string in the orchestrator.
|
|
15
|
-
export const VALIDATION_FAILED_CODE = '"ValidationFailed"';
|
|
16
|
-
const MAX_RETRY_AFTER_MS = 60_000;
|
|
17
|
-
function defaultSleep(delayMs) {
|
|
18
|
-
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
19
|
-
}
|
|
20
|
-
function isTransientStatus(status) {
|
|
21
|
-
return status >= 500 && status <= 599;
|
|
22
|
-
}
|
|
23
|
-
function retryAfterMs(response) {
|
|
24
|
-
const value = response.headers?.get('retry-after')?.trim();
|
|
25
|
-
if (!value)
|
|
26
|
-
return undefined;
|
|
27
|
-
const seconds = Number(value);
|
|
28
|
-
const delayMs = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(value) - Date.now();
|
|
29
|
-
if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > MAX_RETRY_AFTER_MS)
|
|
30
|
-
return undefined;
|
|
31
|
-
return Math.round(delayMs);
|
|
32
|
-
}
|
|
33
|
-
function errorMessage(error) {
|
|
34
|
-
return error instanceof Error && error.message ? error.message : String(error);
|
|
35
|
-
}
|
|
36
|
-
// The generated client parses non-2xx bodies to JSON internally (unlike a raw
|
|
37
|
-
// fetch(), which hands back the raw body string). Re-serializing here keeps
|
|
38
|
-
// ApiError's `body: string` contract and parsePreviewValidationErrors'
|
|
39
|
-
// `JSON.parse` unchanged for callers, at the cost of a harmless double-parse
|
|
40
|
-
// on error paths only.
|
|
41
|
-
function stringifyError(error) {
|
|
42
|
-
return typeof error === 'string' ? error : JSON.stringify(error ?? {});
|
|
43
|
-
}
|
|
44
|
-
// Cap on the body slice appended to ApiError.message. Bumped from 1000 →
|
|
45
|
-
// 16384 so realistic 422 ValidationFailed reports (which list every
|
|
46
|
-
// offending component, ~100 chars per error, easily exceeds 1KB once you
|
|
47
|
-
// cross ~10 components) survive intact through subprocess stderr. The
|
|
48
|
-
// orchestrator's parseOffendingComponentNames does JSON.parse on this slice
|
|
49
|
-
// and silently fails to recover any offenders if the JSON is mid-truncated.
|
|
50
|
-
// The cap stays in place to keep a runaway server response from blowing up
|
|
51
|
-
// log output.
|
|
52
|
-
const ERROR_BODY_LOG_CAP = 16384;
|
|
53
|
-
export class ApiError extends Error {
|
|
54
|
-
status;
|
|
55
|
-
body;
|
|
56
|
-
guidance;
|
|
57
|
-
constructor(message, status, body, guidance) {
|
|
58
|
-
super(message);
|
|
59
|
-
this.status = status;
|
|
60
|
-
this.body = body;
|
|
61
|
-
this.guidance = guidance;
|
|
62
|
-
if (body) {
|
|
63
|
-
const trimmed = body.length > ERROR_BODY_LOG_CAP ? body.slice(0, ERROR_BODY_LOG_CAP) + '…' : body;
|
|
64
|
-
this.message = `${message}\n${trimmed}`;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const COMPONENT_PATH_PREFIX = 'manifest:components/';
|
|
69
|
-
/**
|
|
70
|
-
* Parse the JSON body of a 422 from `previewImport()` into structured
|
|
71
|
-
* per-component validation errors. Returns [] for any malformed input
|
|
72
|
-
* so callers can fall back to the generic error path without try/catch.
|
|
73
|
-
*
|
|
74
|
-
* Path shape: `manifest:components/<Name>/$slots/<key>` or
|
|
75
|
-
* `manifest:components/<Name>/$properties/<key>`. Only the component
|
|
76
|
-
* name is extracted today; `path` and `message` are kept verbatim so
|
|
77
|
-
* future surfaces (debug logging, headless retry in SP-4) can render
|
|
78
|
-
* the field-level detail.
|
|
79
|
-
*/
|
|
80
|
-
export function parsePreviewValidationErrors(body) {
|
|
81
|
-
if (!body)
|
|
82
|
-
return [];
|
|
83
|
-
let parsed;
|
|
84
|
-
try {
|
|
85
|
-
parsed = JSON.parse(body);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
const details = parsed?.details;
|
|
91
|
-
const errors = details?.errors;
|
|
92
|
-
if (!Array.isArray(errors))
|
|
93
|
-
return [];
|
|
94
|
-
const out = [];
|
|
95
|
-
for (const raw of errors) {
|
|
96
|
-
if (typeof raw !== 'object' || raw === null)
|
|
97
|
-
continue;
|
|
98
|
-
const entry = raw;
|
|
99
|
-
if (typeof entry.path !== 'string' || typeof entry.message !== 'string')
|
|
100
|
-
continue;
|
|
101
|
-
if (!entry.path.startsWith(COMPONENT_PATH_PREFIX))
|
|
102
|
-
continue;
|
|
103
|
-
const tail = entry.path.slice(COMPONENT_PATH_PREFIX.length);
|
|
104
|
-
const slash = tail.indexOf('/');
|
|
105
|
-
const componentName = slash === -1 ? tail : tail.slice(0, slash);
|
|
106
|
-
if (!componentName)
|
|
107
|
-
continue;
|
|
108
|
-
out.push({ componentName, path: entry.path, message: entry.message });
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
const PROPERTY_BREAKING_REASONS = new Set([
|
|
113
|
-
'removed',
|
|
114
|
-
'added_required_no_default',
|
|
115
|
-
'type_changed',
|
|
116
|
-
'validation_narrowed',
|
|
117
|
-
]);
|
|
118
|
-
const SLOT_BREAKING_REASONS = new Set(['slot_removed', 'slot_allowed_components_narrowed']);
|
|
119
|
-
export function sanitizeBreakingChanges(raw) {
|
|
120
|
-
if (!Array.isArray(raw))
|
|
121
|
-
return [];
|
|
122
|
-
const out = [];
|
|
123
|
-
for (const bc of raw) {
|
|
124
|
-
if (typeof bc !== 'object' || bc === null)
|
|
125
|
-
continue;
|
|
126
|
-
const reason = bc.reason;
|
|
127
|
-
if (typeof reason !== 'string')
|
|
128
|
-
continue;
|
|
129
|
-
if ('propertyId' in bc && typeof bc.propertyId === 'string') {
|
|
130
|
-
if (PROPERTY_BREAKING_REASONS.has(reason))
|
|
131
|
-
out.push(bc);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if ('slotId' in bc && typeof bc.slotId === 'string') {
|
|
135
|
-
if (SLOT_BREAKING_REASONS.has(reason))
|
|
136
|
-
out.push(bc);
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
function sanitizePreviewResponse(res) {
|
|
143
|
-
for (const item of res.components?.changed ?? []) {
|
|
144
|
-
const cc = item.changeClassification;
|
|
145
|
-
if (cc && Array.isArray(cc.breakingChanges)) {
|
|
146
|
-
cc.breakingChanges = sanitizeBreakingChanges(cc.breakingChanges);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return res;
|
|
150
|
-
}
|
|
151
|
-
function isApsDenialBody(body) {
|
|
152
|
-
if (!body)
|
|
153
|
-
return false;
|
|
154
|
-
try {
|
|
155
|
-
const parsed = JSON.parse(body);
|
|
156
|
-
const id = parsed?.sys?.id;
|
|
157
|
-
return id === 'NotFound' || id === 'AccessDenied';
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
async function request(url, options) {
|
|
164
|
-
const headers = {
|
|
165
|
-
Authorization: `Bearer ${options.token}`,
|
|
166
|
-
'Content-Type': 'application/json',
|
|
167
|
-
'X-Contentful-User-Agent': buildUserAgent(),
|
|
168
|
-
};
|
|
169
|
-
const init = {
|
|
170
|
-
method: options.method ?? 'GET',
|
|
171
|
-
headers,
|
|
172
|
-
};
|
|
173
|
-
if (options.body)
|
|
174
|
-
init.body = options.body;
|
|
175
|
-
return fetch(url, init);
|
|
176
|
-
}
|
|
177
|
-
export class ImportApiClient {
|
|
178
|
-
host;
|
|
179
|
-
token;
|
|
180
|
-
spaceId;
|
|
181
|
-
environmentId;
|
|
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
|
-
}
|
|
192
|
-
constructor(opts) {
|
|
193
|
-
this.host = toApiHost(opts.host);
|
|
194
|
-
this.token = opts.cmaToken;
|
|
195
|
-
this.spaceId = opts.spaceId;
|
|
196
|
-
this.environmentId = opts.environmentId;
|
|
197
|
-
const initialDelayMs = Math.max(0, opts.retry?.initialDelayMs ?? 250);
|
|
198
|
-
this.retry = {
|
|
199
|
-
maxAttempts: Math.max(1, Math.floor(opts.retry?.maxAttempts ?? 3)),
|
|
200
|
-
initialDelayMs,
|
|
201
|
-
maxDelayMs: Math.max(initialDelayMs, opts.retry?.maxDelayMs ?? 2000),
|
|
202
|
-
sleep: opts.retry?.sleep ?? defaultSleep,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
base() {
|
|
206
|
-
return `${this.host}/spaces/${this.spaceId}/environments/${this.environmentId}`;
|
|
207
|
-
}
|
|
208
|
-
headers() {
|
|
209
|
-
return {
|
|
210
|
-
Authorization: `Bearer ${this.token}`,
|
|
211
|
-
'Content-Type': 'application/json',
|
|
212
|
-
'X-Contentful-User-Agent': buildUserAgent(),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
async requestWithRetry(phase, errorPrefix, makeCall, context = {}) {
|
|
216
|
-
const debug = getDebugLogger();
|
|
217
|
-
const startedAt = Date.now();
|
|
218
|
-
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
219
|
-
let result;
|
|
220
|
-
try {
|
|
221
|
-
result = await makeCall();
|
|
222
|
-
}
|
|
223
|
-
catch (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
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (!isTransientStatus(result.response.status)) {
|
|
250
|
-
this.noteRequestId(result.response);
|
|
251
|
-
return result;
|
|
252
|
-
}
|
|
253
|
-
if (attempt === this.retry.maxAttempts) {
|
|
254
|
-
const body = stringifyError(result.error);
|
|
255
|
-
const guidance = `The ${phase} request failed after ${attempt} attempts because the service remained unavailable. ` +
|
|
256
|
-
'Wait a moment and try again.';
|
|
257
|
-
debug.event('apply', `${phase}.error`, {
|
|
258
|
-
...context,
|
|
259
|
-
attempt,
|
|
260
|
-
maxAttempts: this.retry.maxAttempts,
|
|
261
|
-
status: result.response.status,
|
|
262
|
-
reason: 'retry_exhausted',
|
|
263
|
-
durationMs: Date.now() - startedAt,
|
|
264
|
-
bodyHead: body.slice(0, 2000),
|
|
265
|
-
});
|
|
266
|
-
throw new ApiError(`${errorPrefix} ${result.response.status}`, result.response.status, body, guidance);
|
|
267
|
-
}
|
|
268
|
-
const backoffMs = Math.min(this.retry.initialDelayMs * 2 ** (attempt - 1), this.retry.maxDelayMs);
|
|
269
|
-
const delayMs = retryAfterMs(result.response) ?? backoffMs;
|
|
270
|
-
debug.event('apply', `${phase}.retry`, {
|
|
271
|
-
...context,
|
|
272
|
-
attempt,
|
|
273
|
-
maxAttempts: this.retry.maxAttempts,
|
|
274
|
-
status: result.response.status,
|
|
275
|
-
reason: 'http_5xx',
|
|
276
|
-
delayMs,
|
|
277
|
-
});
|
|
278
|
-
await this.retry.sleep(delayMs);
|
|
279
|
-
}
|
|
280
|
-
throw new Error('Retry attempts exhausted');
|
|
281
|
-
}
|
|
282
|
-
async validateToken() {
|
|
283
|
-
// /users/me is the canonical token-validity endpoint — avoids space-membership
|
|
284
|
-
// false positives that don't apply to the design-systems API authorization path.
|
|
285
|
-
const url = `${this.host}/users/me`;
|
|
286
|
-
const res = await request(url, { token: this.token });
|
|
287
|
-
this.noteRequestId(res);
|
|
288
|
-
if (res.status === 401) {
|
|
289
|
-
throw new ApiError('CMA token is invalid or revoked', res.status, await res.text());
|
|
290
|
-
}
|
|
291
|
-
if (!res.ok) {
|
|
292
|
-
throw new ApiError(`unexpected error validating token: ${res.status}`, res.status, await res.text());
|
|
293
|
-
}
|
|
294
|
-
await this.checkPreflight();
|
|
295
|
-
}
|
|
296
|
-
// Probe the sources-api preflight endpoint to catch APS-denied users up-front,
|
|
297
|
-
// rather than letting them run a full apply flow that only fails at the write.
|
|
298
|
-
// The endpoint gates Component + DesignToken permissions and returns:
|
|
299
|
-
// 200 → user has apply permissions, proceed
|
|
300
|
-
// 403 { sys.id: AccessDenied } → user lacks write; surface via error-parser
|
|
301
|
-
// 404 { sys.id: NotFound } → user lacks read (APS hides existence)
|
|
302
|
-
// 404 without a NotFound envelope → older backend deployment; treat as pass
|
|
303
|
-
// 5xx / network → backend flake; treat as pass, do not block
|
|
304
|
-
async checkPreflight() {
|
|
305
|
-
const url = `${this.host}/spaces/${this.spaceId}/environments/${this.environmentId}/design_systems/preflight`;
|
|
306
|
-
let res;
|
|
307
|
-
try {
|
|
308
|
-
res = await request(url, { token: this.token });
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
return; // network error — don't block; the real call will surface it
|
|
312
|
-
}
|
|
313
|
-
this.noteRequestId(res);
|
|
314
|
-
if (res.ok)
|
|
315
|
-
return;
|
|
316
|
-
if (res.status >= 500)
|
|
317
|
-
return; // backend flake — don't block
|
|
318
|
-
const body = await res.text();
|
|
319
|
-
if (res.status === 404 && !isApsDenialBody(body))
|
|
320
|
-
return; // legacy backend without the endpoint
|
|
321
|
-
throw new ApiError(`preflight failed: ${res.status}`, res.status, body);
|
|
322
|
-
}
|
|
323
|
-
async previewImport(manifest, allowDeletions = false) {
|
|
324
|
-
const debug = getDebugLogger();
|
|
325
|
-
const startedAt = Date.now();
|
|
326
|
-
debug.event('apply', 'preview.request', {
|
|
327
|
-
url: `${this.base()}/design_systems/imports/preview`,
|
|
328
|
-
componentCount: manifest.components?.length ?? 0,
|
|
329
|
-
tokenCount: manifest.designTokens?.length ?? 0,
|
|
330
|
-
allowDeletions,
|
|
331
|
-
});
|
|
332
|
-
const result = await this.requestWithRetry('preview', PREVIEW_ERROR_PREFIX, () => designSystemImportSourcelessPreview({
|
|
333
|
-
baseUrl: this.host,
|
|
334
|
-
headers: this.headers(),
|
|
335
|
-
path: { spaceId: this.spaceId, environmentId: this.environmentId },
|
|
336
|
-
body: { ...manifest, allowDeletions },
|
|
337
|
-
parseAs: 'json',
|
|
338
|
-
}));
|
|
339
|
-
if (!result.response.ok) {
|
|
340
|
-
const body = stringifyError(result.error);
|
|
341
|
-
debug.event('apply', 'preview.error', {
|
|
342
|
-
status: result.response.status,
|
|
343
|
-
durationMs: Date.now() - startedAt,
|
|
344
|
-
bodyHead: body.slice(0, 2000),
|
|
345
|
-
});
|
|
346
|
-
throw new ApiError(`${PREVIEW_ERROR_PREFIX} ${result.response.status}`, result.response.status, body);
|
|
347
|
-
}
|
|
348
|
-
const parsed = result.data;
|
|
349
|
-
debug.event('apply', 'preview.ok', { status: result.response.status, durationMs: Date.now() - startedAt });
|
|
350
|
-
return sanitizePreviewResponse(parsed);
|
|
351
|
-
}
|
|
352
|
-
async applyImport(manifest, options) {
|
|
353
|
-
const { acknowledgeBreakingChanges, allowDeletions = false } = options;
|
|
354
|
-
const debug = getDebugLogger();
|
|
355
|
-
const startedAt = Date.now();
|
|
356
|
-
debug.event('apply', 'apply.request', {
|
|
357
|
-
url: `${this.base()}/design_systems/imports/apply`,
|
|
358
|
-
acknowledgeBreakingChanges,
|
|
359
|
-
allowDeletions,
|
|
360
|
-
});
|
|
361
|
-
let result;
|
|
362
|
-
try {
|
|
363
|
-
result = await designSystemImportApply({
|
|
364
|
-
baseUrl: this.host,
|
|
365
|
-
headers: this.headers(),
|
|
366
|
-
path: { spaceId: this.spaceId, environmentId: this.environmentId },
|
|
367
|
-
body: { ...manifest, acknowledgeBreakingChanges, allowDeletions },
|
|
368
|
-
parseAs: 'json',
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
const body = `The apply request was not retried because its outcome is unknown and retrying could start a duplicate operation. ` +
|
|
373
|
-
`Check for an existing operation before trying again. Cause: ${errorMessage(error)}`;
|
|
374
|
-
debug.event('apply', 'apply.error', {
|
|
375
|
-
status: 0,
|
|
376
|
-
durationMs: Date.now() - startedAt,
|
|
377
|
-
reason: 'transport_error_not_retried',
|
|
378
|
-
});
|
|
379
|
-
throw new ApiError(`${APPLY_ERROR_PREFIX} 0`, 0, body);
|
|
380
|
-
}
|
|
381
|
-
if (!result.response.ok) {
|
|
382
|
-
const body = stringifyError(result.error);
|
|
383
|
-
const guidance = isTransientStatus(result.response.status)
|
|
384
|
-
? '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.'
|
|
385
|
-
: undefined;
|
|
386
|
-
debug.event('apply', 'apply.error', {
|
|
387
|
-
status: result.response.status,
|
|
388
|
-
durationMs: Date.now() - startedAt,
|
|
389
|
-
bodyHead: body.slice(0, 2000),
|
|
390
|
-
});
|
|
391
|
-
throw new ApiError(`${APPLY_ERROR_PREFIX} ${result.response.status}`, result.response.status, body, guidance);
|
|
392
|
-
}
|
|
393
|
-
const parsed = result.data;
|
|
394
|
-
debug.event('apply', 'apply.accepted', {
|
|
395
|
-
status: result.response.status,
|
|
396
|
-
operationId: parsed.sys?.id,
|
|
397
|
-
durationMs: Date.now() - startedAt,
|
|
398
|
-
});
|
|
399
|
-
return parsed;
|
|
400
|
-
}
|
|
401
|
-
async pollOperation(operationId, opts = {}) {
|
|
402
|
-
const intervalMs = opts.intervalMs ?? 2000;
|
|
403
|
-
const maxIntervalMs = opts.maxIntervalMs ?? Math.round(intervalMs * 2.5);
|
|
404
|
-
const maxAttempts = opts.maxAttempts ?? 150;
|
|
405
|
-
const terminalStatuses = new Set(['succeeded', 'partial', 'failed']);
|
|
406
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
407
|
-
const result = await this.requestWithRetry('poll', 'poll failed:', () => designSystemImportGetOperation({
|
|
408
|
-
baseUrl: this.host,
|
|
409
|
-
headers: this.headers(),
|
|
410
|
-
path: { spaceId: this.spaceId, environmentId: this.environmentId, operationId },
|
|
411
|
-
parseAs: 'json',
|
|
412
|
-
}), { operationId });
|
|
413
|
-
if (!result.response.ok) {
|
|
414
|
-
throw new ApiError(`poll failed: ${result.response.status}`, result.response.status, stringifyError(result.error));
|
|
415
|
-
}
|
|
416
|
-
const op = result.data;
|
|
417
|
-
opts.onProgress?.(op);
|
|
418
|
-
getDebugLogger().event('apply', 'poll.tick', {
|
|
419
|
-
operationId,
|
|
420
|
-
attempt,
|
|
421
|
-
status: op.sys.status,
|
|
422
|
-
});
|
|
423
|
-
if (terminalStatuses.has(op.sys.status)) {
|
|
424
|
-
getDebugLogger().event('apply', 'poll.terminal', {
|
|
425
|
-
operationId,
|
|
426
|
-
attempt,
|
|
427
|
-
status: op.sys.status,
|
|
428
|
-
});
|
|
429
|
-
return op;
|
|
430
|
-
}
|
|
431
|
-
if (attempt < maxAttempts - 1) {
|
|
432
|
-
const progress = maxAttempts > 1 ? attempt / (maxAttempts - 1) : 0;
|
|
433
|
-
const baseDelay = intervalMs + (maxIntervalMs - intervalMs) * progress;
|
|
434
|
-
const jitter = Math.random() * baseDelay * 0.15;
|
|
435
|
-
await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
throw new Error(`Operation ${operationId} timed out after ${maxAttempts} attempts`);
|
|
439
|
-
}
|
|
440
|
-
}
|