@contentful/experience-design-system-cli 2.5.3-dev-build-b2e98f1.0 → 2.6.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @contentful/experience-design-system-cli
2
2
 
3
- CLI for extracting, reviewing, generating, validating, and pushing Contentful Experience Design System component definitions.
3
+ CLI for extracting, reviewing, generating, validating, and pushing Contentful Experience Design System component definitions into Experiences
4
4
 
5
- ## Pipeline Overview
5
+ ## CLI Overview
6
6
 
7
7
  The commands form a pipeline. Run them in order, or use `import` to orchestrate the whole thing at once:
8
8
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.5.3-dev-build-b2e98f1.0",
3
+ "version": "2.6.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,10 @@
18
18
  "node": "./dist/src/index.js"
19
19
  }
20
20
  },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://npm.pkg.github.com/"
24
+ },
21
25
  "files": [
22
26
  "bin/",
23
27
  "dist/",
@@ -1,4 +1,5 @@
1
- export const DEFAULT_HOST = 'https://api.contentful.com';
1
+ import { DEFAULT_API_HOST, toApiHost } from '../host-utils.js';
2
+ export const DEFAULT_HOST = DEFAULT_API_HOST;
2
3
  export class ApiError extends Error {
3
4
  status;
4
5
  body;
@@ -33,7 +34,7 @@ export class ImportApiClient {
33
34
  spaceId;
34
35
  environmentId;
35
36
  constructor(opts) {
36
- this.host = opts.host ?? DEFAULT_HOST;
37
+ this.host = toApiHost(opts.host);
37
38
  this.token = opts.cmaToken;
38
39
  this.spaceId = opts.spaceId;
39
40
  this.environmentId = opts.environmentId;
@@ -2,6 +2,7 @@ export type ExperiencesCredentials = {
2
2
  spaceId: string;
3
3
  environmentId: string;
4
4
  cmaToken: string;
5
+ host?: string;
5
6
  };
6
7
  export declare function readExperiencesCredentials(): Promise<ExperiencesCredentials>;
7
8
  export declare function writeExperiencesCredentials(creds: ExperiencesCredentials): Promise<void>;
@@ -1,29 +1,39 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import { toConfiguredHost } from './host-utils.js';
4
5
  const CREDENTIALS_DIR = join(homedir(), '.config', 'experiences');
5
6
  const CREDENTIALS_PATH = join(CREDENTIALS_DIR, 'credentials.json');
6
7
  export async function readExperiencesCredentials() {
7
8
  try {
8
9
  const raw = await readFile(CREDENTIALS_PATH, 'utf8');
9
10
  const parsed = JSON.parse(raw);
11
+ const host = toConfiguredHost(process.env['EDS_HOST'] ?? parsed.host);
10
12
  return {
11
13
  spaceId: process.env['CONTENTFUL_SPACE_ID'] ?? parsed.spaceId ?? '',
12
14
  environmentId: process.env['CONTENTFUL_ENVIRONMENT_ID'] ?? parsed.environmentId ?? '',
13
15
  cmaToken: process.env['CONTENTFUL_MANAGEMENT_TOKEN'] ?? parsed.cmaToken ?? '',
16
+ ...(host ? { host } : {}),
14
17
  };
15
18
  }
16
19
  catch {
20
+ const host = toConfiguredHost(process.env['EDS_HOST']);
17
21
  return {
18
22
  spaceId: process.env['CONTENTFUL_SPACE_ID'] ?? '',
19
23
  environmentId: process.env['CONTENTFUL_ENVIRONMENT_ID'] ?? '',
20
24
  cmaToken: process.env['CONTENTFUL_MANAGEMENT_TOKEN'] ?? '',
25
+ ...(host ? { host } : {}),
21
26
  };
22
27
  }
23
28
  }
24
29
  export async function writeExperiencesCredentials(creds) {
30
+ const { host: _host, ...rest } = creds;
31
+ const host = toConfiguredHost(creds.host);
25
32
  await mkdir(CREDENTIALS_DIR, { recursive: true });
26
- await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
33
+ await writeFile(CREDENTIALS_PATH, JSON.stringify({
34
+ ...rest,
35
+ ...(host ? { host } : {}),
36
+ }, null, 2) + '\n', { mode: 0o600 });
27
37
  }
28
38
  export function experiencesCredentialsPath() {
29
39
  return CREDENTIALS_PATH;
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_API_HOST = "https://api.contentful.com";
2
+ export declare const DEFAULT_CONFIGURED_HOST = "api.contentful.com";
3
+ export declare function toConfiguredHost(host?: string): string | undefined;
4
+ export declare function toApiHost(host?: string): string;
@@ -0,0 +1,26 @@
1
+ export const DEFAULT_API_HOST = 'https://api.contentful.com';
2
+ export const DEFAULT_CONFIGURED_HOST = 'api.contentful.com';
3
+ function trimTrailingSlashes(value) {
4
+ return value.replace(/\/+$/, '');
5
+ }
6
+ function normalizeHostInput(host) {
7
+ const value = host?.trim();
8
+ if (!value)
9
+ return undefined;
10
+ return trimTrailingSlashes(value);
11
+ }
12
+ export function toConfiguredHost(host) {
13
+ const normalized = normalizeHostInput(host);
14
+ if (!normalized)
15
+ return undefined;
16
+ return normalized.replace(/^https:\/\//i, '');
17
+ }
18
+ export function toApiHost(host) {
19
+ const normalized = normalizeHostInput(host);
20
+ if (!normalized)
21
+ return DEFAULT_API_HOST;
22
+ if (/^[a-z][a-z\d+\-.]*:\/\//i.test(normalized)) {
23
+ return normalized;
24
+ }
25
+ return `https://${normalized}`;
26
+ }
@@ -1,6 +1,7 @@
1
1
  import { resolve, join } from 'node:path';
2
2
  import { runPipeline } from './orchestrator.js';
3
3
  import { readExperiencesCredentials } from '../credentials-store.js';
4
+ import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
4
5
  export function registerImportCommand(program) {
5
6
  program
6
7
  .command('import')
@@ -45,6 +46,7 @@ export function registerImportCommand(program) {
45
46
  initialSpaceId: creds.spaceId,
46
47
  initialEnvironmentId: creds.environmentId || 'master',
47
48
  initialCmaToken: creds.cmaToken,
49
+ initialHost: toConfiguredHost(opts.host ?? creds.host) ?? DEFAULT_CONFIGURED_HOST,
48
50
  initialAgent: opts.agent !== 'claude' ? opts.agent : undefined,
49
51
  initialProjectPath: opts.project !== '.' ? resolve(opts.project) : undefined,
50
52
  host: opts.host,
@@ -3,8 +3,9 @@ export type WizardAppProps = {
3
3
  initialSpaceId?: string;
4
4
  initialEnvironmentId?: string;
5
5
  initialCmaToken?: string;
6
+ initialHost?: string;
6
7
  initialAgent?: string;
7
8
  initialProjectPath?: string;
8
9
  host?: string;
9
10
  };
10
- export declare function WizardApp({ initialSpaceId, initialEnvironmentId, initialCmaToken, initialAgent, initialProjectPath, host, }?: WizardAppProps): React.ReactElement;
11
+ export declare function WizardApp({ initialSpaceId, initialEnvironmentId, initialCmaToken, initialHost, initialAgent, initialProjectPath, host, }?: WizardAppProps): React.ReactElement;
@@ -24,6 +24,8 @@ import { buildManifest } from '@contentful/experience-design-system-types';
24
24
  import { openPipelineDb, loadCDFComponents, seedCDFFromPreviewResponse, seedDefaultsFromChangedItems, backfillUnclassifiedProps, } from '../../session/db.js';
25
25
  import { checkAgentAuth } from '../../generate/agent-runner.js';
26
26
  import { normalizePath } from '../path-utils.js';
27
+ import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../../host-utils.js';
28
+ import { writeExperiencesCredentials } from '../../credentials-store.js';
27
29
  function findCliPath() {
28
30
  return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'bin', 'cli.js');
29
31
  }
@@ -39,8 +41,9 @@ function logStep(entry) {
39
41
  const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
40
42
  appendFileSync(WIZARD_LOG, line);
41
43
  }
42
- export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialAgent, initialProjectPath, host, } = {}) {
43
- const apiHost = host ?? process.env['EDS_HOST'];
44
+ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialProjectPath, host, } = {}) {
45
+ const defaultConfiguredHost = toConfiguredHost(host || process.env['EDS_HOST']) ?? DEFAULT_CONFIGURED_HOST;
46
+ const resolveWizardHost = (hostValue) => hostValue || defaultConfiguredHost;
44
47
  const { stdout } = useStdout();
45
48
  const terminalWidth = stdout?.columns ?? 80;
46
49
  const logInit = useRef(false);
@@ -75,6 +78,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
75
78
  spaceId: initialSpaceId,
76
79
  environmentId: initialEnvironmentId,
77
80
  cmaToken: initialCmaToken,
81
+ host: resolveWizardHost(toConfiguredHost(initialHost)),
78
82
  credentialsError: '',
79
83
  serverPreview: null,
80
84
  manifest: null,
@@ -375,7 +379,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
375
379
  }
376
380
  if (returnToPreview) {
377
381
  const { extractSessionId, tokensPath } = sessionRef.current;
378
- void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken);
382
+ void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
379
383
  }
380
384
  else {
381
385
  advanceToPushFlow(generatedAcceptedCount);
@@ -431,19 +435,46 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
431
435
  }
432
436
  // Re-preview with updated definitions
433
437
  const { extractSessionId: sid, tokensPath: tp } = sessionRef.current;
434
- void runPreview(sid, tp, state.spaceId, state.environmentId, state.cmaToken);
438
+ void runPreview(sid, tp, state.spaceId, state.environmentId, state.cmaToken, state.host);
435
439
  };
436
- const confirmCredentials = (spaceId, environmentId, cmaToken) => {
440
+ const advanceWithCredentials = (spaceId, environmentId, cmaToken, host) => {
441
+ const resolvedHost = resolveWizardHost(host);
437
442
  credentialsRef.current = { spaceId, environmentId, cmaToken };
438
- update({ spaceId, environmentId, cmaToken, step: 'credential-test-gate' });
443
+ update({
444
+ spaceId,
445
+ environmentId,
446
+ cmaToken,
447
+ host: resolvedHost,
448
+ credentialsError: '',
449
+ step: 'credential-test-gate',
450
+ });
451
+ };
452
+ const confirmCredentials = async (spaceId, environmentId, cmaToken, host) => {
453
+ const resolvedHost = resolveWizardHost(host);
454
+ try {
455
+ await writeExperiencesCredentials({ spaceId, environmentId, cmaToken, host: resolvedHost });
456
+ advanceWithCredentials(spaceId, environmentId, cmaToken, resolvedHost);
457
+ }
458
+ catch (e) {
459
+ const message = e instanceof Error ? e.message : 'Unable to save credentials';
460
+ update({
461
+ spaceId,
462
+ environmentId,
463
+ cmaToken,
464
+ host: resolvedHost,
465
+ credentialsError: `Failed to save credentials: ${message}`,
466
+ step: 'credentials',
467
+ });
468
+ }
439
469
  };
440
- const validateCredentials = async (spaceId, environmentId, cmaToken) => {
470
+ const validateCredentials = async (spaceId, environmentId, cmaToken, host) => {
441
471
  update({ step: 'validating-credentials' });
442
472
  try {
443
- const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
473
+ const resolvedHost = resolveWizardHost(host);
474
+ const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: resolvedHost });
444
475
  await client.validateToken();
445
476
  const { extractSessionId, tokensPath } = sessionRef.current;
446
- void runPreview(extractSessionId, tokensPath, spaceId, environmentId, cmaToken);
477
+ void runPreview(extractSessionId, tokensPath, spaceId, environmentId, cmaToken, resolvedHost);
447
478
  }
448
479
  catch (e) {
449
480
  if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
@@ -459,10 +490,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
459
490
  });
460
491
  }
461
492
  };
462
- const runPreview = async (extractSessionId, tokensPath, spaceId, environmentId, cmaToken) => {
493
+ const runPreview = async (extractSessionId, tokensPath, spaceId, environmentId, cmaToken, host) => {
463
494
  update({ step: 'previewing' });
495
+ const resolvedHost = resolveWizardHost(host);
464
496
  try {
465
- const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
497
+ const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: resolvedHost });
466
498
  let components = [];
467
499
  if (extractSessionId) {
468
500
  const db = openPipelineDb();
@@ -548,7 +580,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
548
580
  errorMessage: `Not found (404). Check that the space ID, environment ID, and host are correct.\n\n` +
549
581
  ` Space: ${spaceId}\n` +
550
582
  ` Environment: ${environmentId}\n` +
551
- (apiHost ? ` Host: ${apiHost}\n` : '') +
583
+ (resolvedHost ? ` Host: ${resolvedHost}\n` : '') +
552
584
  `\nIf using a custom --host, make sure the space exists on that host.`,
553
585
  });
554
586
  return;
@@ -560,7 +592,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
560
592
  update({ step: 'error', errorStep: 'apply preview', errorMessage: msg, errorAllowCredentialRetry: true });
561
593
  }
562
594
  };
563
- const runPush = async (manifest, spaceId, environmentId, cmaToken, acknowledgeBreakingChanges, preview) => {
595
+ const runPush = async (manifest, spaceId, environmentId, cmaToken, host, acknowledgeBreakingChanges, preview) => {
564
596
  if (preview) {
565
597
  const hasComponentChanges = preview.components.new.length > 0 ||
566
598
  preview.components.changed.length > 0 ||
@@ -579,7 +611,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
579
611
  }
580
612
  update({ step: 'pushing' });
581
613
  try {
582
- const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
614
+ const resolvedHost = resolveWizardHost(host);
615
+ const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: resolvedHost });
583
616
  let operation = await client.applyImport(manifest, acknowledgeBreakingChanges);
584
617
  try {
585
618
  logStep({
@@ -835,13 +868,15 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
835
868
  }, onQuit: () => process.exit(0) }));
836
869
  }
837
870
  case 'credentials':
838
- return (_jsx(CredentialsStep, { initialSpaceId: state.spaceId, initialEnvironmentId: state.environmentId, initialCmaToken: state.cmaToken, error: state.credentialsError || undefined, onConfirm: confirmCredentials, onContinue: confirmCredentials, onQuit: () => process.exit(0) }));
871
+ return (_jsx(CredentialsStep, { initialSpaceId: state.spaceId, initialEnvironmentId: state.environmentId, initialCmaToken: state.cmaToken, initialHost: state.host, error: state.credentialsError || undefined, onConfirm: (spaceId, environmentId, cmaToken, host) => {
872
+ void confirmCredentials(spaceId, environmentId, cmaToken, host);
873
+ }, onContinue: advanceWithCredentials, onQuit: () => process.exit(0) }));
839
874
  case 'credential-test-gate':
840
875
  return (_jsx(GateStep, { successMessage: "Credentials entered", summary: `Space: ${state.spaceId} · Environment: ${state.environmentId}`, context: "Verify your credentials work before running the import, or skip and find out during the push step.", continueLabel: "Test credentials", skipLabel: "Skip and continue", showSkip: true, onContinue: () => {
841
- void validateCredentials(state.spaceId, state.environmentId, state.cmaToken);
876
+ void validateCredentials(state.spaceId, state.environmentId, state.cmaToken, state.host);
842
877
  }, onSkip: () => {
843
878
  const { extractSessionId, tokensPath } = sessionRef.current;
844
- void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken);
879
+ void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken, state.host);
845
880
  }, onQuit: () => process.exit(0) }));
846
881
  case 'validating-credentials':
847
882
  return (_jsx(RunningStep, { stepNumber: totalSteps - 1, totalSteps: totalSteps, title: "Validating credentials", description: "Checking that your space ID and CMA token are valid..." }));
@@ -849,7 +884,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
849
884
  return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Computing diff", description: "Computing diff against your Contentful space..." }));
850
885
  case 'preview-gate':
851
886
  return (_jsx(WizardPreviewStep, { preview: state.serverPreview, spaceId: state.spaceId, environmentId: state.environmentId, stepNumber: totalSteps, totalSteps: totalSteps, onConfirm: (acknowledge) => {
852
- void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, acknowledge, state.serverPreview);
887
+ void runPush(state.manifest, state.spaceId, state.environmentId, state.cmaToken, state.host, acknowledge, state.serverPreview);
853
888
  }, onEdit: () => {
854
889
  void runEditFromPreview(state.serverPreview);
855
890
  }, onSaveFiles: () => {
@@ -5,11 +5,12 @@ type CredentialsStepProps = {
5
5
  initialSpaceId?: string;
6
6
  initialEnvironmentId?: string;
7
7
  initialCmaToken?: string;
8
- /** Called when the user submits with all fields changed from their initial values */
9
- onConfirm: (spaceId: string, environmentId: string, cmaToken: string) => void;
8
+ initialHost?: string;
9
+ /** Called when the user submits with any field changed from its initial value */
10
+ onConfirm: (spaceId: string, environmentId: string, cmaToken: string, host: string) => void;
10
11
  /** Called when the user submits without changing any field (use existing creds as-is) */
11
- onContinue?: (spaceId: string, environmentId: string, cmaToken: string) => void;
12
+ onContinue?: (spaceId: string, environmentId: string, cmaToken: string, host: string) => void;
12
13
  onQuit: () => void;
13
14
  };
14
- export declare function CredentialsStep({ summary, error: externalError, initialSpaceId, initialEnvironmentId, initialCmaToken, onConfirm, onContinue, onQuit, }: CredentialsStepProps): React.ReactElement;
15
+ export declare function CredentialsStep({ summary, error: externalError, initialSpaceId, initialEnvironmentId, initialCmaToken, initialHost, onConfirm, onContinue, onQuit, }: CredentialsStepProps): React.ReactElement;
15
16
  export {};
@@ -2,10 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
5
- export function CredentialsStep({ summary, error: externalError, initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', onConfirm, onContinue, onQuit, }) {
5
+ import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../../../host-utils.js';
6
+ export function CredentialsStep({ summary, error: externalError, initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, onConfirm, onContinue, onQuit, }) {
7
+ const normalizedInitialHost = toConfiguredHost(initialHost) ?? DEFAULT_CONFIGURED_HOST;
6
8
  const [spaceId, setSpaceId] = useState(initialSpaceId);
7
9
  const [environmentId, setEnvironmentId] = useState(initialEnvironmentId);
8
10
  const [cmaToken, setCmaToken] = useState(initialCmaToken);
11
+ const [host, setHost] = useState(normalizedInitialHost);
9
12
  const [activeField, setActiveField] = useState('spaceId');
10
13
  const [inlineError, setInlineError] = useState(null);
11
14
  const [cursorVisible, setCursorVisible] = useState(true);
@@ -23,25 +26,31 @@ export function CredentialsStep({ summary, error: externalError, initialSpaceId
23
26
  setActiveField('cmaToken');
24
27
  return;
25
28
  }
29
+ if (activeField === 'cmaToken') {
30
+ setActiveField('host');
31
+ return;
32
+ }
26
33
  // Submit
27
34
  if (!spaceId.trim() || !environmentId.trim() || !cmaToken.trim()) {
28
35
  setInlineError('All fields are required.');
29
36
  return;
30
37
  }
31
38
  setInlineError(null);
39
+ const submittedHost = toConfiguredHost(host) ?? DEFAULT_CONFIGURED_HOST;
32
40
  const unchanged = spaceId.trim() === initialSpaceId &&
33
41
  environmentId.trim() === initialEnvironmentId &&
34
- cmaToken.trim() === initialCmaToken;
42
+ cmaToken.trim() === initialCmaToken &&
43
+ submittedHost === normalizedInitialHost;
35
44
  if (unchanged && onContinue) {
36
- onContinue(spaceId.trim(), environmentId.trim(), cmaToken.trim());
45
+ onContinue(spaceId.trim(), environmentId.trim(), cmaToken.trim(), submittedHost);
37
46
  }
38
47
  else {
39
- onConfirm(spaceId.trim(), environmentId.trim(), cmaToken.trim());
48
+ onConfirm(spaceId.trim(), environmentId.trim(), cmaToken.trim(), submittedHost);
40
49
  }
41
50
  return;
42
51
  }
43
52
  if (key.tab) {
44
- setActiveField((f) => (f === 'spaceId' ? 'environmentId' : f === 'environmentId' ? 'cmaToken' : 'spaceId'));
53
+ setActiveField((f) => f === 'spaceId' ? 'environmentId' : f === 'environmentId' ? 'cmaToken' : f === 'cmaToken' ? 'host' : 'spaceId');
45
54
  return;
46
55
  }
47
56
  if (key.escape || input === 'q') {
@@ -53,8 +62,10 @@ export function CredentialsStep({ summary, error: externalError, initialSpaceId
53
62
  setSpaceId((v) => v.slice(0, -1));
54
63
  else if (activeField === 'environmentId')
55
64
  setEnvironmentId((v) => v.slice(0, -1));
56
- else
65
+ else if (activeField === 'cmaToken')
57
66
  setCmaToken((v) => v.slice(0, -1));
67
+ else
68
+ setHost((v) => v.slice(0, -1));
58
69
  return;
59
70
  }
60
71
  if (input && !key.ctrl && !key.meta) {
@@ -62,18 +73,21 @@ export function CredentialsStep({ summary, error: externalError, initialSpaceId
62
73
  setSpaceId((v) => v + input);
63
74
  else if (activeField === 'environmentId')
64
75
  setEnvironmentId((v) => v + input);
65
- else
76
+ else if (activeField === 'cmaToken')
66
77
  setCmaToken((v) => v + input);
78
+ else
79
+ setHost((v) => v + input);
67
80
  }
68
81
  });
69
82
  const cursor = cursorVisible ? '█' : ' ';
70
83
  function renderField(label, value, field, masked = false) {
71
84
  const isActive = activeField === field;
72
85
  const display = masked ? '•'.repeat(value.length) : value;
73
- return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : undefined, children: '?' }), _jsxs(Text, { bold: isActive, children: [label, ":"] }), _jsx(Text, { children: isActive ? display + cursor : display || _jsx(Text, { dimColor: true, children: "(empty)" }) })] }));
86
+ const fallback = field === 'host' ? DEFAULT_CONFIGURED_HOST : _jsx(Text, { dimColor: true, children: "(empty)" });
87
+ return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : undefined, children: '?' }), _jsxs(Text, { bold: isActive, children: [label, ":"] }), _jsx(Text, { children: isActive ? display + cursor : display || fallback })] }));
74
88
  }
75
89
  const displayError = inlineError ?? externalError ?? null;
76
90
  return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [summary && _jsxs(Text, { color: "green", children: ["\u2713 ", summary] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: initialSpaceId && initialCmaToken
77
91
  ? 'Credentials pre-filled from experiences setup. Press Enter to continue or edit any field to update.'
78
- : 'Enter your Contentful credentials to continue.' }) }), !(initialSpaceId && initialCmaToken) && (_jsx(Text, { dimColor: true, children: "Tip: run experiences setup to save these to ~/.config/experiences/credentials.json so they pre-fill here automatically." })), _jsxs(Box, { flexDirection: "column", gap: 0, marginTop: 1, children: [renderField('Space ID', spaceId, 'spaceId'), renderField('Environment', environmentId, 'environmentId'), renderField('CMA Token', cmaToken, 'cmaToken', true)] }), displayError && _jsxs(Text, { color: "red", children: ["\u2717 ", displayError] }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Next field / Submit" }), _jsx(Text, { dimColor: true, children: "[Tab] Switch field" }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
92
+ : 'Enter your Contentful credentials to continue.' }) }), !(initialSpaceId && initialCmaToken) && (_jsx(Text, { dimColor: true, children: "Tip: run experiences setup to save these to ~/.config/experiences/credentials.json so they pre-fill here automatically." })), _jsxs(Box, { flexDirection: "column", gap: 0, marginTop: 1, children: [renderField('Space ID', spaceId, 'spaceId'), renderField('Environment', environmentId, 'environmentId'), renderField('CMA Token', cmaToken, 'cmaToken', true), renderField('API Host', host, 'host')] }), activeField === 'host' && _jsx(Text, { dimColor: true, children: "Default: api.contentful.com \u00B7 EU spaces: api.eu.contentful.com" }), displayError && _jsxs(Text, { color: "red", children: ["\u2717 ", displayError] }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Next field / Submit" }), _jsx(Text, { dimColor: true, children: "[Tab] Switch field" }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
79
93
  }
package/dist/src/index.js CHANGED
@@ -1,2 +1,7 @@
1
1
  import { createProgram } from './program.js';
2
- await createProgram().parseAsync();
2
+ createProgram()
3
+ .parseAsync()
4
+ .catch((err) => {
5
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
6
+ process.exit(1);
7
+ });
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { promisify } from 'node:util';
8
8
  import { readExperiencesCredentials, writeExperiencesCredentials, experiencesCredentialsPath, } from '../credentials-store.js';
9
+ import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
9
10
  const execFileAsync = promisify(execFile);
10
11
  const REQUIRED_NODE_MAJOR = 24;
11
12
  // ── Output helpers ────────────────────────────────────────────────────────────
@@ -29,7 +30,13 @@ function dim(msg) {
29
30
  process.stdout.write(`\x1b[2m${msg}\x1b[0m\n`);
30
31
  }
31
32
  // ── Prompt helpers ────────────────────────────────────────────────────────────
33
+ function isInteractivePromptSession() {
34
+ return !!(process.stdin.isTTY && process.stdout.isTTY);
35
+ }
32
36
  function prompt(question) {
37
+ if (!isInteractivePromptSession()) {
38
+ return Promise.resolve('');
39
+ }
33
40
  const rl = createInterface({ input: process.stdin, output: process.stdout });
34
41
  return new Promise((resolve) => {
35
42
  rl.question(question, (answer) => {
@@ -39,6 +46,9 @@ function prompt(question) {
39
46
  });
40
47
  }
41
48
  function promptSecret(question) {
49
+ if (!isInteractivePromptSession()) {
50
+ return Promise.resolve('');
51
+ }
42
52
  // Use readline for all prompts — mixing raw-mode stdin listeners with
43
53
  // readline createInterface causes readline to buffer+unshift unconsumed
44
54
  // input back onto the stream, which the raw listener then re-reads,
@@ -51,9 +61,10 @@ function promptSecret(question) {
51
61
  });
52
62
  let value = '';
53
63
  process.stdout.write(question);
64
+ let origWrite = null;
54
65
  if (process.stdin.isTTY) {
55
66
  // Intercept the readline output write so we can replace echoed chars with *
56
- const origWrite = rl.output.write.bind(rl.output);
67
+ origWrite = rl.output.write.bind(rl.output);
57
68
  rl.output.write = (s) => {
58
69
  // Allow newline through; suppress everything else (the echoed characters)
59
70
  if (s === '\r\n' || s === '\n' || s === '\r')
@@ -65,12 +76,24 @@ function promptSecret(question) {
65
76
  rl.close();
66
77
  });
67
78
  rl.once('close', () => {
68
- process.stdout.write('\n');
79
+ // Restore stdout.write before resolving — the interceptor patches rl.output.write
80
+ // which is process.stdout.write, so without restoring it all subsequent output is swallowed.
81
+ if (origWrite) {
82
+ rl.output.write = origWrite;
83
+ }
84
+ // In TTY mode readline already emitted \n when Enter was pressed; only add one in non-TTY.
85
+ if (!process.stdin.isTTY)
86
+ process.stdout.write('\n');
87
+ // rl.close() pauses stdin; resume it so subsequent prompt() calls work.
88
+ process.stdin.resume();
69
89
  resolve(value);
70
90
  });
71
91
  });
72
92
  }
73
93
  async function confirm(question, defaultYes = true) {
94
+ if (!isInteractivePromptSession()) {
95
+ return false;
96
+ }
74
97
  const hint = defaultYes ? '[Y/n]' : '[y/N]';
75
98
  const answer = await prompt(` ${question} ${hint} `);
76
99
  if (!answer)
@@ -387,6 +410,8 @@ async function setupContentfulCredentials() {
387
410
  const currentSpace = stored.spaceId;
388
411
  const currentEnv = stored.environmentId;
389
412
  const currentToken = stored.cmaToken;
413
+ const storedHost = stored.host;
414
+ const currentHost = storedHost ?? DEFAULT_CONFIGURED_HOST;
390
415
  const hasAny = !!(currentSpace || currentEnv || currentToken);
391
416
  if (hasAny) {
392
417
  info('Current values:');
@@ -408,6 +433,7 @@ async function setupContentfulCredentials() {
408
433
  else {
409
434
  warn('CMA Token (not set)');
410
435
  }
436
+ ok(`API Host ${currentHost}`);
411
437
  info('');
412
438
  }
413
439
  const allSet = !!(currentSpace && currentEnv && currentToken);
@@ -434,9 +460,12 @@ async function setupContentfulCredentials() {
434
460
  warn('Space ID and CMA token are required. Skipped.');
435
461
  return false;
436
462
  }
437
- await writeExperiencesCredentials({ spaceId, environmentId, cmaToken });
463
+ const hostInput = await prompt(` API host [${currentHost}]: `);
464
+ const host = toConfiguredHost(hostInput) ?? storedHost;
465
+ await writeExperiencesCredentials({ spaceId, environmentId, cmaToken, ...(host ? { host } : {}) });
438
466
  ok(`Credentials saved to ${experiencesCredentialsPath()}`);
439
- info('Run experiences import the credentials step will be pre-filled automatically.');
467
+ ok(`API host set to ${host ?? DEFAULT_CONFIGURED_HOST}`);
468
+ info('Run experiences import — credentials will be pre-filled automatically.');
440
469
  return true;
441
470
  }
442
471
  // ── Step 6: Optional quality-of-life ─────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.5.3-dev-build-b2e98f1.0",
3
+ "version": "2.6.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,10 @@
18
18
  "node": "./dist/src/index.js"
19
19
  }
20
20
  },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://npm.pkg.github.com/"
24
+ },
21
25
  "files": [
22
26
  "bin/",
23
27
  "dist/",
@@ -32,7 +36,7 @@
32
36
  "react-dom": "^18.3.1",
33
37
  "ts-morph": "^27.0.2",
34
38
  "typescript": "^5.9.3",
35
- "@contentful/experience-design-system-types": "2.5.3-dev-build-b2e98f1.0"
39
+ "@contentful/experience-design-system-types": "2.6.0"
36
40
  },
37
41
  "devDependencies": {
38
42
  "@tsconfig/node24": "^24.0.3",