@contentful/experience-design-system-cli 2.12.1-dev-build-0b7de73.0 → 2.12.1

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.
Files changed (39) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/apply/api-client.js +45 -4
  3. package/dist/src/apply/command.js +1 -12
  4. package/dist/src/apply/preview-utils.d.ts +12 -0
  5. package/dist/src/apply/preview-utils.js +22 -0
  6. package/dist/src/credentials-store.d.ts +16 -0
  7. package/dist/src/credentials-store.js +22 -5
  8. package/dist/src/generate/agent-runner.js +40 -5
  9. package/dist/src/generate/command.js +2 -2
  10. package/dist/src/import/auto-filter-resolve.d.ts +0 -9
  11. package/dist/src/import/auto-filter-resolve.js +5 -5
  12. package/dist/src/import/command.js +2 -1
  13. package/dist/src/import/orchestrator.js +15 -2
  14. package/dist/src/import/picker-dispatch.d.ts +8 -0
  15. package/dist/src/import/picker-dispatch.js +19 -4
  16. package/dist/src/import/tui/WizardApp.d.ts +8 -2
  17. package/dist/src/import/tui/WizardApp.js +86 -19
  18. package/dist/src/import/tui/final-review-host.d.ts +8 -1
  19. package/dist/src/import/tui/final-review-host.js +2 -2
  20. package/dist/src/import/tui/push-progress.d.ts +0 -14
  21. package/dist/src/import/tui/push-progress.js +1 -14
  22. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -1
  23. package/dist/src/import/tui/steps/GenerateReviewStep.js +16 -3
  24. package/dist/src/import/tui/steps/PushingStep.d.ts +2 -3
  25. package/dist/src/import/tui/steps/PushingStep.js +3 -20
  26. package/dist/src/lib/debug-logger.d.ts +49 -0
  27. package/dist/src/lib/debug-logger.js +229 -0
  28. package/dist/src/lib/debug-preamble.d.ts +14 -0
  29. package/dist/src/lib/debug-preamble.js +33 -0
  30. package/dist/src/program.js +26 -0
  31. package/dist/src/runs/modify-launcher.d.ts +2 -0
  32. package/dist/src/runs/modify-launcher.js +2 -0
  33. package/dist/src/runs/push-launcher.d.ts +52 -0
  34. package/dist/src/runs/push-launcher.js +77 -0
  35. package/dist/src/runs/replay-helpers.js +19 -7
  36. package/dist/src/setup/command.js +31 -1
  37. package/dist/src/setup/debug-mode-prompt.d.ts +11 -0
  38. package/dist/src/setup/debug-mode-prompt.js +22 -0
  39. package/package.json +2 -2
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { execFile, spawn } from 'node:child_process';
10
10
  import { mkdir } from 'node:fs/promises';
11
11
  import { buildRunTeaserLine } from './run-teaser.js';
12
+ import { getDebugLogger } from '../../lib/debug-logger.js';
12
13
  import { PathPrompt } from '../../runs/path-prompt.js';
13
14
  import { RunPicker } from '../../runs/run-picker.js';
14
15
  import { SaveConflictGate } from '../../runs/save-conflict.js';
@@ -28,7 +29,6 @@ import { ErrorStep } from './steps/ErrorStep.js';
28
29
  import { TokenInputStep } from './steps/TokenInputStep.js';
29
30
  import { PreviewValidationErrorStep } from './steps/PreviewValidationErrorStep.js';
30
31
  import { PushingStep } from './steps/PushingStep.js';
31
- import { computePushExpected } from './push-progress.js';
32
32
  import { nextStateAfterPrint } from './run-print-files-helpers.js';
33
33
  import { PushDecisionGateStep } from './steps/PushDecisionGateStep.js';
34
34
  import { chooseGateAction } from './push-decision-gate-helpers.js';
@@ -37,6 +37,7 @@ import { handlePreview422, applySkipValidationErrors, clearedValidationErrorStat
37
37
  import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
38
38
  import { spawnGenerateChild } from './spawn-generate.js';
39
39
  import { readTokensFromPath, hasBreakingChangesWithImpact } from '../../apply/manifest.js';
40
+ import { isEmptyPreview } from '../../apply/preview-utils.js';
40
41
  import { buildManifest } from '@contentful/experience-design-system-types';
41
42
  import { openPipelineDb, loadCDFComponents, loadScopeComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
42
43
  import { ScopeGateHost } from './scope-gate-host.js';
@@ -147,8 +148,10 @@ const WIZARD_LOG = join(tmpdir(), 'experiences-import-wizard.log');
147
148
  function logStep(entry) {
148
149
  const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
149
150
  appendFileSync(WIZARD_LOG, line);
151
+ // Tee into the unified debug log when enabled — cheap no-op otherwise.
152
+ getDebugLogger().event('wizard', 'step', entry);
150
153
  }
151
- export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope = false, noCache = false, autoFilter = true, livePreview = true, noPush = false, noSave = false, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, initialStep, initialRawTokensPath, initialRuns, onRunPicked, } = {}) {
154
+ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope = false, noCache = false, autoFilter = true, livePreview = true, noPush = false, noSave = false, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, } = {}) {
152
155
  const defaultConfiguredHost = toConfiguredHost(host || process.env['EDS_HOST']) ?? DEFAULT_CONFIGURED_HOST;
153
156
  const resolveWizardHost = (hostValue) => hostValue || defaultConfiguredHost;
154
157
  const { stdout } = useStdout();
@@ -182,22 +185,31 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
182
185
  // GenerateReviewStep loads its data off `state.extractSessionId`, so all
183
186
  // we need to do here is seed the IDs and the step.
184
187
  const modifyEntryReady = !!seedExtractSessionId && initialStep === 'final-review';
188
+ // Push-from-picker entry: the run-picker's Push action mounts the wizard
189
+ // with seeded session IDs and `initialStep: 'push-from-picker'` so the
190
+ // operator sees the same preview + push UX as a fresh import. Skips
191
+ // welcome, token-input, scope-gate, final-review, and push-decision-gate;
192
+ // an effect below dispatches `runPreview` on mount which drives
193
+ // previewing → pushing → done.
194
+ const pushFromPickerReady = !!seedExtractSessionId && initialStep === 'push-from-picker';
185
195
  // Headless raw-tokens entry: when the operator passed `--raw-tokens <path>`
186
196
  // the CLI seeds this prop. Skip welcome + token-input and land on the
187
197
  // `generating-tokens` step which already drives the token-classification
188
198
  // subprocess off `state.rawTokensPath`. Modify-entry wins if both are set.
189
- const rawTokensEntryReady = !modifyEntryReady && !!initialRawTokensPath;
199
+ const rawTokensEntryReady = !modifyEntryReady && !pushFromPickerReady && !!initialRawTokensPath;
190
200
  const initialStepResolved = modifyEntryReady
191
201
  ? 'final-review'
192
- : rawTokensEntryReady
193
- ? 'generating-tokens'
194
- : initialProjectPath
195
- ? 'token-input'
196
- : 'welcome';
202
+ : pushFromPickerReady
203
+ ? 'push-from-picker'
204
+ : rawTokensEntryReady
205
+ ? 'generating-tokens'
206
+ : initialProjectPath
207
+ ? 'token-input'
208
+ : 'welcome';
197
209
  const initialOutDir = initialProjectPath ? join(resolve(initialProjectPath), '.contentful') : '';
198
- const initialTokensPath = modifyEntryReady && initialOutDir ? join(initialOutDir, 'tokens.json') : '';
210
+ const initialTokensPath = (modifyEntryReady || pushFromPickerReady) && initialOutDir ? join(initialOutDir, 'tokens.json') : '';
199
211
  const [state, setState] = useState({
200
- step: modifyEntryReady || rawTokensEntryReady
212
+ step: modifyEntryReady || rawTokensEntryReady || pushFromPickerReady
201
213
  ? initialStepResolved
202
214
  : initialRuns && initialRuns.length > 0
203
215
  ? 'run-picker'
@@ -207,7 +219,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
207
219
  projectPath: initialProjectPath ?? '',
208
220
  outDir: initialOutDir,
209
221
  rawTokensPath: rawTokensEntryReady ? initialRawTokensPath : '',
210
- tokensPath: initialTokensPath,
222
+ tokensPath: seedTokensPath ?? initialTokensPath,
211
223
  tokenSourceChanged: null,
212
224
  skipComponents: false,
213
225
  tokenSessionId: seedTokenSessionId ?? null,
@@ -231,7 +243,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
231
243
  serverPreview: null,
232
244
  manifest: null,
233
245
  pushProgress: null,
234
- pushExpected: null,
235
246
  pushResult: {
236
247
  componentTypes: { created: 0, updated: 0, removed: 0, failed: 0 },
237
248
  designTokens: { created: 0, updated: 0, removed: 0, failed: 0 },
@@ -251,6 +262,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
251
262
  generatePrefetchError: null,
252
263
  credentialsSkipped: false,
253
264
  lastRunId: null,
265
+ finalizeErrorBanner: null,
254
266
  });
255
267
  useEffect(() => {
256
268
  sessionRef.current = {
@@ -357,7 +369,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
357
369
  });
358
370
  return;
359
371
  }
360
- const sessionMatch = /^session:\s*(.+)$/m.exec(result.stdout);
372
+ const sessionMatch = /^session=(.+)$/m.exec(result.stdout);
361
373
  const tokenSessionId = sessionMatch ? sessionMatch[1].trim() : null;
362
374
  const tokensPath = join(outDir, 'tokens.json');
363
375
  const printArgs = ['print', 'tokens', '--out', tokensPath];
@@ -906,7 +918,27 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
906
918
  db.close();
907
919
  }
908
920
  }
909
- update({ step: 'preview-gate', serverPreview: preview, manifest, ...clearedValidationErrorState() });
921
+ // INTEG-4411 refined: preview-aware finalize guard. If the resulting
922
+ // push would be a pure no-op across every diff bucket, don't send an
923
+ // empty manifest to EDSI — route back to `final-review` with an inline
924
+ // banner. Accepted components, rejections that remove server-side
925
+ // components, and token-only diffs all keep this branch from firing.
926
+ if (isEmptyPreview(preview)) {
927
+ update({
928
+ step: 'final-review',
929
+ finalizeErrorBanner: 'Nothing to push — accept a component, reject a component that exists in Contentful, or quit.',
930
+ serverPreview: preview,
931
+ ...clearedValidationErrorState(),
932
+ });
933
+ return;
934
+ }
935
+ update({
936
+ step: 'preview-gate',
937
+ serverPreview: preview,
938
+ manifest,
939
+ finalizeErrorBanner: null,
940
+ ...clearedValidationErrorState(),
941
+ });
910
942
  }
911
943
  catch (e) {
912
944
  if (e instanceof ApiError) {
@@ -1002,8 +1034,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1002
1034
  return;
1003
1035
  }
1004
1036
  }
1005
- const pushExpected = preview ? computePushExpected(preview) : null;
1006
- update({ step: 'pushing', pushExpected, pushProgress: null });
1037
+ update({ step: 'pushing', pushProgress: null });
1007
1038
  try {
1008
1039
  const resolvedHost = resolveWizardHost(host);
1009
1040
  const client = new ImportApiClient({
@@ -1303,6 +1334,22 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1303
1334
  })();
1304
1335
  }
1305
1336
  }, [state.step]); // intentional: only re-run when step changes
1337
+ // Push-from-picker entry: on mount, dispatch runPreview to jump through
1338
+ // previewing → preview-gate → pushing → done. We DON'T wait for the
1339
+ // operator to interact with any pre-preview screen; the run-picker Push
1340
+ // action is a "click Push and watch it happen" flow. `preview-gate` still
1341
+ // renders after runPreview resolves — that's the diff-review screen where
1342
+ // the operator can confirm/quit/edit. We route straight past
1343
+ // push-decision-gate (which is for save-vs-push decisions on a fresh run).
1344
+ const pushFromPickerDispatched = useRef(false);
1345
+ useEffect(() => {
1346
+ if (state.step !== 'push-from-picker')
1347
+ return;
1348
+ if (pushFromPickerDispatched.current)
1349
+ return;
1350
+ pushFromPickerDispatched.current = true;
1351
+ void runPreview(state.extractSessionId, state.tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
1352
+ }, [state.step]);
1306
1353
  // ── Render ────────────────────────────────────────────────────────────────────────────
1307
1354
  const noQuitSteps = [
1308
1355
  'run-picker',
@@ -1313,6 +1360,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1313
1360
  'generating',
1314
1361
  'printing',
1315
1362
  'previewing',
1363
+ 'push-from-picker',
1316
1364
  'pushing',
1317
1365
  ];
1318
1366
  const hints = noQuitSteps.includes(state.step) ? [] : [{ key: 'q', label: 'quit' }];
@@ -1454,8 +1502,16 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1454
1502
  return (_jsx(RunningStep, { stepNumber: stepNum, totalSteps: totalSteps, title: "Generating definitions", description: `${formatAcceptanceSummary({ accepted: state.acceptedCount, autoRejected: state.autoRejectedCount })} ${state.agent} is mapping your TypeScript types to Contentful's CDF format.${hasTokens ? ' Using your design tokens for prop resolution.' : ''}`, detail: progressDetail }));
1455
1503
  }
1456
1504
  case 'final-review': {
1457
- return (_jsx(FinalReviewHost, { extractSessionId: state.extractSessionId, generatedCount: state.generatedCount, autoAccept: autoAcceptScope, livePreview: livePreview, spaceId: state.spaceId, environmentId: state.environmentId, cmaToken: state.cmaToken, host: state.host, tokensPath: state.tokensPath, onFinalize: (accepted, rejected, unresolved) => {
1505
+ return (_jsx(FinalReviewHost, { extractSessionId: state.extractSessionId, generatedCount: state.generatedCount, autoAccept: autoAcceptScope, livePreview: livePreview, spaceId: state.spaceId, environmentId: state.environmentId, cmaToken: state.cmaToken, host: state.host, tokensPath: state.tokensPath, initialFinalizeError: state.finalizeErrorBanner, onFinalize: (accepted, rejected, unresolved) => {
1458
1506
  process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected} Unresolved: ${unresolved}\n`);
1507
+ // INTEG-4411 refined: no `accepted === 0` up-front block here.
1508
+ // A zero-accepted finalize can still be a valid push when the
1509
+ // operator explicitly rejected component(s) that exist server-
1510
+ // side (→ REMOVALS) or when tokens carry a diff. The load-
1511
+ // bearing no-op check consults the preview response inside
1512
+ // `runPreview` below (see `isEmptyPreview` branch) and routes
1513
+ // back to `final-review` with `finalizeErrorBanner` set when
1514
+ // the diff is truly empty.
1459
1515
  if (noPush) {
1460
1516
  update({ generatedAcceptedCount: accepted });
1461
1517
  void startSaveFlow();
@@ -1507,7 +1563,17 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1507
1563
  case 'credentials':
1508
1564
  return (_jsx(CredentialsStep, { initialSpaceId: state.spaceId, initialEnvironmentId: state.environmentId, initialCmaToken: state.cmaToken, initialHost: state.host, error: state.credentialsError || undefined, validating: state.credentialsValidating, generatePrefetchStatus: state.generatePrefetchStatus, generatePrefetchError: state.generatePrefetchError, onConfirm: (spaceId, environmentId, cmaToken, host) => {
1509
1565
  void confirmCredentials(spaceId, environmentId, cmaToken, host);
1510
- }, onContinue: advanceWithCredentials, onRetryPrefetch: state.generatePrefetchStatus === 'failed' && sessionRef.current.extractSessionId
1566
+ },
1567
+ // INTEG-4410: unify the unchanged-form path with the changed-form
1568
+ // path so credentials are ALWAYS persisted to disk on submit.
1569
+ // The pre-fix wiring routed `onContinue` at
1570
+ // `advanceWithCredentials`, which only mutated state — so on
1571
+ // `--modify` (where the wizard seeds `run.pushedTo` into the
1572
+ // form, not disk) an operator pressing Enter never wrote to
1573
+ // ~/.config/experiences/credentials.json and disk stayed stale.
1574
+ onContinue: (spaceId, environmentId, cmaToken, host) => {
1575
+ void confirmCredentials(spaceId, environmentId, cmaToken, host);
1576
+ }, onRetryPrefetch: state.generatePrefetchStatus === 'failed' && sessionRef.current.extractSessionId
1511
1577
  ? () => {
1512
1578
  const sid = sessionRef.current.extractSessionId;
1513
1579
  void startGeneratePrefetch(sid, state.tokensPath);
@@ -1535,6 +1601,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1535
1601
  // value is kept in the union for back-compat with existing imports/tests
1536
1602
  // but should never actually be set — `validateCredentials` keeps the
1537
1603
  // step on 'credentials' and toggles `credentialsValidating` instead.
1604
+ case 'push-from-picker':
1538
1605
  case 'previewing':
1539
1606
  return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Computing diff", description: "Computing diff against your Contentful space..." }));
1540
1607
  case 'preview-gate':
@@ -1546,7 +1613,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1546
1613
  void startSaveFlow();
1547
1614
  }, onQuit: () => process.exit(0) }));
1548
1615
  case 'pushing':
1549
- return (_jsx(PushingStep, { stepNumber: totalSteps, totalSteps: totalSteps, expected: state.pushExpected, progress: state.pushProgress }));
1616
+ return _jsx(PushingStep, { stepNumber: totalSteps, totalSteps: totalSteps, progress: state.pushProgress });
1550
1617
  case 'path-prompt':
1551
1618
  return (_jsx(PathPrompt, { defaultPath: state.outDir, onSubmit: (submitted) => {
1552
1619
  void (async () => {
@@ -11,5 +11,12 @@ export type FinalReviewHostProps = {
11
11
  cmaToken?: string;
12
12
  host?: string;
13
13
  tokensPath?: string;
14
+ /**
15
+ * INTEG-4411 refined: message displayed as an inline banner when the wizard
16
+ * routes back here after the preview API returned an empty diff (pure
17
+ * no-op push). Cleared on the next `a` / `A` keystroke inside
18
+ * GenerateReviewStep.
19
+ */
20
+ initialFinalizeError?: string | null;
14
21
  };
15
- export declare function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, }: FinalReviewHostProps): React.ReactElement;
22
+ export declare function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }: FinalReviewHostProps): React.ReactElement;
@@ -2,14 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import React from 'react';
4
4
  import { GenerateReviewStep } from './steps/GenerateReviewStep.js';
5
- export function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, }) {
5
+ export function FinalReviewHost({ extractSessionId, generatedCount, autoAccept, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }) {
6
6
  if (!extractSessionId) {
7
7
  return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { color: "red", children: "Error: no session ID \u2014 cannot load generated definitions." }) }));
8
8
  }
9
9
  if (autoAccept) {
10
10
  return _jsx(FinalReviewAutoAccept, { generatedCount: generatedCount, onFinalize: onFinalize });
11
11
  }
12
- return (_jsx(GenerateReviewStep, { extractSessionId: extractSessionId, onFinalize: onFinalize, onQuit: onQuit, livePreview: livePreview, spaceId: spaceId, environmentId: environmentId, cmaToken: cmaToken, host: host, tokensPath: tokensPath }));
12
+ return (_jsx(GenerateReviewStep, { extractSessionId: extractSessionId, onFinalize: onFinalize, onQuit: onQuit, livePreview: livePreview, spaceId: spaceId, environmentId: environmentId, cmaToken: cmaToken, host: host, tokensPath: tokensPath, initialFinalizeError: initialFinalizeError }));
13
13
  }
14
14
  function FinalReviewAutoAccept({ generatedCount, onFinalize, }) {
15
15
  React.useEffect(() => {
@@ -1,16 +1,3 @@
1
- import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
2
- export type PushExpected = {
3
- componentTypes: {
4
- create: number;
5
- update: number;
6
- remove: number;
7
- };
8
- designTokens: {
9
- create: number;
10
- update: number;
11
- remove: number;
12
- };
13
- };
14
1
  export type PushProgress = {
15
2
  kind: 'queued';
16
3
  operationId: string;
@@ -20,4 +7,3 @@ export type PushProgress = {
20
7
  total: number;
21
8
  current: string | null;
22
9
  } | null;
23
- export declare function computePushExpected(preview: ServerPreviewResponse): PushExpected;
@@ -1,14 +1 @@
1
- export function computePushExpected(preview) {
2
- return {
3
- componentTypes: {
4
- create: preview.components?.new?.length ?? 0,
5
- update: preview.components?.changed?.length ?? 0,
6
- remove: preview.components?.removed?.length ?? 0,
7
- },
8
- designTokens: {
9
- create: preview.tokens?.new?.length ?? 0,
10
- update: preview.tokens?.changed?.length ?? 0,
11
- remove: preview.tokens?.removed?.length ?? 0,
12
- },
13
- };
14
- }
1
+ export {};
@@ -16,6 +16,13 @@ type GenerateReviewStepProps = {
16
16
  cmaToken?: string;
17
17
  host?: string;
18
18
  tokensPath?: string;
19
+ /**
20
+ * INTEG-4411 refined: initial value for the inline `finalizeError` banner.
21
+ * The wizard sets this when it routes back to `final-review` after the
22
+ * preview API returned an empty diff (pure no-op push). Cleared on the
23
+ * next `a` / `A` keystroke.
24
+ */
25
+ initialFinalizeError?: string | null;
19
26
  };
20
27
  /**
21
28
  * Sort components for the final-review sidebar so the underlying data array
@@ -31,5 +38,5 @@ export declare function sortComponentsForSidebar<T extends {
31
38
  key: string;
32
39
  entry: CDFComponentEntry;
33
40
  }>(components: T[]): T[];
34
- export declare function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, }: GenerateReviewStepProps): React.ReactElement;
41
+ export declare function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }: GenerateReviewStepProps): React.ReactElement;
35
42
  export {};
@@ -35,7 +35,7 @@ export function sortComponentsForSidebar(components) {
35
35
  }
36
36
  const VISIBLE_COUNT = 20;
37
37
  const PANEL_HEIGHT = 22;
38
- export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', }) {
38
+ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', initialFinalizeError = null, }) {
39
39
  const { stdout } = useStdout();
40
40
  const terminalWidth = stdout?.columns ?? 80;
41
41
  const [components, setComponents] = useState([]);
@@ -51,6 +51,9 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
51
51
  const [showJson, setShowJson] = useState(false);
52
52
  const [draftValue, setDraftValue] = useState('');
53
53
  const [saveError, setSaveError] = useState(null);
54
+ // INTEG-4411: inline banner shown when the operator tries to finalize
55
+ // with zero accepted components. Cleared on the next 'a' or 'A' press.
56
+ const [finalizeError, setFinalizeError] = useState(initialFinalizeError);
54
57
  // Feature 1: per-component review metadata (rationale + source location)
55
58
  // for the currently-selected component. Reloaded when selection changes.
56
59
  const [reviewMetadata, setReviewMetadata] = useState(null);
@@ -192,6 +195,15 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
192
195
  // 'generate-rejected' so loadCDFComponents excludes it from the manifest.
193
196
  // The operator told us they want accept-to-ship semantics — leaving a
194
197
  // component unresolved should NOT silently push it (Pilot-2026-06-24 R2).
198
+ const acceptedCount = components.filter((c) => c.status === 'accepted').length;
199
+ // INTEG-4411 refined: DO NOT block on `acceptedCount === 0` up-front.
200
+ // A push with zero accepted but one or more rejections targeting a
201
+ // component that exists server-side still produces REMOVALS — a valid
202
+ // push, not a no-op. Same for token-only diffs. The load-bearing no-op
203
+ // check lives downstream in WizardApp.runPreview, which consults the
204
+ // preview response and only blocks when every diff bucket is empty.
205
+ // We keep the `finalizeError` state so the wizard can route back here
206
+ // with an inline banner when that downstream check fires.
195
207
  const explicitlyRejected = components.filter((c) => c.status === 'rejected').map((c) => c.key);
196
208
  const unresolved = components.filter((c) => c.status === 'needs-review').map((c) => c.key);
197
209
  const toReject = [...explicitlyRejected, ...unresolved];
@@ -215,7 +227,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
215
227
  db.close();
216
228
  }
217
229
  }
218
- const acceptedCount = components.filter((c) => c.status === 'accepted').length;
219
230
  onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
220
231
  };
221
232
  const handleEditSave = () => {
@@ -405,6 +416,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
405
416
  }
406
417
  if (input === 'a') {
407
418
  updateStatus(selectedIdx, 'accepted');
419
+ setFinalizeError(null);
408
420
  return;
409
421
  }
410
422
  if (input === 'r') {
@@ -413,6 +425,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
413
425
  }
414
426
  if (input === 'A') {
415
427
  setComponents((prev) => prev.map((c) => (c.status === 'needs-review' ? { ...c, status: 'accepted' } : c)));
428
+ setFinalizeError(null);
416
429
  return;
417
430
  }
418
431
  if (input === 'J') {
@@ -513,7 +526,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
513
526
  if (!hasCounts)
514
527
  return null;
515
528
  return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color: "green", children: `${counts.new} new` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "yellow", children: `${counts.changed} changed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { dimColor: true, children: `${counts.removed} removed` }), removedComponents.length > 0 && _jsx(Text, { dimColor: true, children: ' ([d] removed list)' }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "red", bold: true, children: `${counts.breaking} breaking` })] }));
516
- })(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
529
+ })(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: "red", children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
517
530
  const idx = components.findIndex((c) => c.key === id);
518
531
  if (idx >= 0) {
519
532
  setSelectedIdx(idx);
@@ -1,10 +1,9 @@
1
1
  import React from 'react';
2
- import type { PushExpected, PushProgress } from '../push-progress.js';
2
+ import type { PushProgress } from '../push-progress.js';
3
3
  type PushingStepProps = {
4
4
  stepNumber: number;
5
5
  totalSteps: number;
6
- expected: PushExpected | null;
7
6
  progress: PushProgress;
8
7
  };
9
- export declare function PushingStep({ stepNumber, totalSteps, expected, progress }: PushingStepProps): React.ReactElement;
8
+ export declare function PushingStep({ stepNumber, totalSteps, progress }: PushingStepProps): React.ReactElement;
10
9
  export {};
@@ -1,25 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
- const ACTION_LABELS = {
6
- create: 'Creating',
7
- update: 'Updating',
8
- remove: 'Deleting',
9
- };
10
- function ActionRow({ label, expectedCount, progress, }) {
11
- // Per spec: do NOT fake proportional tallies. Left side stays "?/N" until
12
- // terminal status surfaces real per-action counts.
13
- const left = progress && progress.kind === 'progress' ? '?' : '?';
14
- return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { children: [" ", label] }), _jsxs(Text, { dimColor: true, children: [left, "/", expectedCount] })] }));
15
- }
16
- function EntitySection({ title, counts, progress, }) {
17
- const visibleActions = ['create', 'update', 'remove'].filter((k) => counts[k] > 0);
18
- if (visibleActions.length === 0)
19
- return null;
20
- return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: title }), visibleActions.map((k) => (_jsx(ActionRow, { label: ACTION_LABELS[k], expectedCount: counts[k], progress: progress }, k)))] }));
21
- }
22
- export function PushingStep({ stepNumber, totalSteps, expected, progress }) {
5
+ export function PushingStep({ stepNumber, totalSteps, progress }) {
23
6
  const [frame, setFrame] = useState(0);
24
7
  const [elapsed, setElapsed] = useState(0);
25
8
  useEffect(() => {
@@ -36,5 +19,5 @@ export function PushingStep({ stepNumber, totalSteps, expected, progress }) {
36
19
  const operationId = progress && progress.kind === 'queued' ? progress.operationId : null;
37
20
  const showGlobal = progress && progress.kind === 'progress';
38
21
  const showCurrent = progress && progress.kind === 'progress' && progress.current ? progress.current : null;
39
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Step ", stepNumber, " of ", totalSteps] }), _jsx(Text, { bold: true, children: "\u2014" }), _jsx(Text, { bold: true, children: "Push to Contentful" })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(40) })] }), _jsx(Text, { children: "Writing component types and design tokens to your Contentful space..." }), operationId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Operation:" }), _jsx(Text, { children: operationId })] })), expected && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(EntitySection, { title: "Component types", counts: expected.componentTypes, progress: progress }), _jsx(EntitySection, { title: "Design tokens", counts: expected.designTokens, progress: progress })] })), showGlobal && progress && progress.kind === 'progress' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [progress.processed, "/", progress.total, " entities"] })] })), showCurrent && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Now processing:" }), _jsx(Text, { children: showCurrent })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Elapsed: ", elapsedStr] }) })] }));
22
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Step ", stepNumber, " of ", totalSteps] }), _jsx(Text, { bold: true, children: "\u2014" }), _jsx(Text, { bold: true, children: "Push to Contentful" })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(40) })] }), _jsx(Text, { children: "Writing component types and design tokens to your Contentful space..." }), operationId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Operation:" }), _jsx(Text, { children: operationId })] })), showGlobal && progress && progress.kind === 'progress' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [progress.processed, "/", progress.total, " entities"] })] })), showCurrent && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Now processing:" }), _jsx(Text, { children: showCurrent })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Elapsed: ", elapsedStr] }) })] }));
40
23
  }
@@ -0,0 +1,49 @@
1
+ export type DebugCategory = 'config' | 'agent' | 'apply' | 'import' | 'analyze' | 'filter' | 'session' | 'runs' | 'tui' | 'wizard' | 'setup' | 'output' | 'other';
2
+ export interface DebugLogger {
3
+ readonly enabled: boolean;
4
+ readonly path: string | null;
5
+ event(category: DebugCategory, name: string, payload?: Record<string, unknown>): void;
6
+ }
7
+ export declare function redactForDebug(payload: unknown): unknown;
8
+ export interface InitDebugLoggerOptions {
9
+ /** Effective on/off decision (flag > env > config, resolved by caller). */
10
+ enabled: boolean;
11
+ /** Command name — recorded in the session-open event for filtering. */
12
+ command?: string;
13
+ /** Override log root (defaults to ~/.contentful/experience-design-system-cli/debug/). */
14
+ root?: string;
15
+ }
16
+ /**
17
+ * Initialize the process-wide debug logger. Safe to call multiple times —
18
+ * subsequent calls return the existing instance.
19
+ *
20
+ * If EDSI_DEBUG_LOG is already set in the environment, this process joins
21
+ * that log file (used by spawned subprocesses so a whole `experiences import`
22
+ * flow lands in a single file).
23
+ */
24
+ export declare function initDebugLogger(opts: InitDebugLoggerOptions): DebugLogger;
25
+ export declare function getDebugLogger(): DebugLogger;
26
+ /** Test-only: reset the singleton. */
27
+ export declare function __resetDebugLoggerForTest(): void;
28
+ /**
29
+ * Resolve effective debug-mode setting from three sources.
30
+ *
31
+ * Precedence (highest first):
32
+ * 1. `--debug` / `--no-debug` CLI flag
33
+ * 2. `EDSI_DEBUG` env var (truthy: 1, true, yes, on)
34
+ * 3. Persisted `debug` field in credentials.json
35
+ * 4. Default: OFF
36
+ */
37
+ export declare function resolveDebugMode(opts: {
38
+ debug?: boolean;
39
+ }, configDebug?: boolean): boolean;
40
+ /**
41
+ * Emit the bright-green "debug logs at <path>" banner to stderr.
42
+ * No-op when debug is disabled or stderr is not a TTY-friendly stream.
43
+ * Callers should invoke this once at start and once at end of the command.
44
+ */
45
+ export declare function printDebugBanner(logger: DebugLogger, phase: 'start' | 'end'): void;
46
+ /** Directory that contains the current process's debug log, if any. */
47
+ export declare function debugLogPath(): string | null;
48
+ /** Ensure spawned children join the same debug log by including EDSI_DEBUG_LOG in their env. */
49
+ export declare function debugEnvForSubprocess(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;