@contentful/experience-design-system-cli 2.11.0 → 2.11.1-dev-build-96af281.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.11.0",
3
+ "version": "2.11.1-dev-build-96af281.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,6 +3,7 @@ import { appendReviewEvent, getRefineArtifactsRoot, ensureRefineSession, getRefi
3
3
  import { loadReviewInput } from '../select/parser.js';
4
4
  import { buildPrompt } from '../../generate/prompt-builder.js';
5
5
  import { parseSelectToolCallLines, runAgent } from '../../generate/agent-runner.js';
6
+ import { readExperiencesCredentials } from '../../credentials-store.js';
6
7
  import { OutputFormatter, c } from '../../output/format.js';
7
8
  import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
8
9
  import { isAbsolute, resolve } from 'node:path';
@@ -174,18 +175,21 @@ export function registerAnalyzeSelectAgentCommand(program) {
174
175
  .description('Use an AI agent to select components for Contentful Experience Orchestration')
175
176
  .option('--session <id>', 'Session ID from analyze extract (defaults to most recent)')
176
177
  .option('--project-root <path>', 'Project root for resolving component source files')
177
- .requiredOption('--agent <name>', 'Agent to use: claude, codex, opencode, cursor')
178
+ .option('--agent <name>', 'Agent to use: claude, codex, opencode, cursor (defaults to value saved by experiences setup)')
178
179
  .option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
179
180
  .option('--verbose', 'Show full agent output including reasoning text')
180
181
  .option('--dry-run', 'Print the prompt for the first component without invoking the agent')
181
182
  .option('--exclude-invalid', 'Auto-reject components with validation errors instead of failing loud (LLM cannot fix structural issues)')
182
183
  .action(async (opts) => {
183
- if (!VALID_AGENTS.has(opts.agent)) {
184
- process.stderr.write(`Error: unknown agent '${opts.agent}'. Accepted values: claude, codex, opencode, cursor\n`);
184
+ const savedCreds = await readExperiencesCredentials();
185
+ const agentName = opts.agent ?? savedCreds.agent;
186
+ const model = opts.model ?? savedCreds.agentModel;
187
+ if (!agentName || !VALID_AGENTS.has(agentName)) {
188
+ process.stderr.write(`Error: no agent configured. Pass --agent <name> or run experiences setup. Accepted values: claude, codex, opencode, cursor\n`);
185
189
  process.exit(1);
186
190
  return;
187
191
  }
188
- const agent = opts.agent;
192
+ const agent = agentName;
189
193
  const sessionId = resolveSessionId(opts.session);
190
194
  const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
191
195
  const db = openPipelineDb();
@@ -266,7 +270,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
266
270
  process.exit(0);
267
271
  return;
268
272
  }
269
- const selectResults = await selectAllComponents(agent, opts.model, selectionCandidates, opts.verbose ?? false);
273
+ const selectResults = await selectAllComponents(agent, model, selectionCandidates, opts.verbose ?? false);
270
274
  // Build decision map from results. Seed auto-rejections from validation exclusions first.
271
275
  const decisions = new Map();
272
276
  for (const comp of invalidComponents) {
@@ -3,6 +3,8 @@ export type ExperiencesCredentials = {
3
3
  environmentId: string;
4
4
  cmaToken: string;
5
5
  host?: string;
6
+ agent?: string;
7
+ agentModel?: string;
6
8
  };
7
9
  export declare function readExperiencesCredentials(): Promise<ExperiencesCredentials>;
8
10
  export declare function writeExperiencesCredentials(creds: ExperiencesCredentials): Promise<void>;
@@ -14,6 +14,8 @@ export async function readExperiencesCredentials() {
14
14
  environmentId: process.env['CONTENTFUL_ENVIRONMENT_ID'] ?? parsed.environmentId ?? '',
15
15
  cmaToken: process.env['CONTENTFUL_MANAGEMENT_TOKEN'] ?? parsed.cmaToken ?? '',
16
16
  ...(host ? { host } : {}),
17
+ ...(parsed.agent ? { agent: parsed.agent } : {}),
18
+ ...(parsed.agentModel ? { agentModel: parsed.agentModel } : {}),
17
19
  };
18
20
  }
19
21
  catch {
@@ -27,12 +29,14 @@ export async function readExperiencesCredentials() {
27
29
  }
28
30
  }
29
31
  export async function writeExperiencesCredentials(creds) {
30
- const { host: _host, ...rest } = creds;
32
+ const { host: _host, agent, agentModel, ...rest } = creds;
31
33
  const host = toConfiguredHost(creds.host);
32
34
  await mkdir(CREDENTIALS_DIR, { recursive: true });
33
35
  await writeFile(CREDENTIALS_PATH, JSON.stringify({
34
36
  ...rest,
35
37
  ...(host ? { host } : {}),
38
+ ...(agent ? { agent } : {}),
39
+ ...(agentModel ? { agentModel } : {}),
36
40
  }, null, 2) + '\n', { mode: 0o600 });
37
41
  }
38
42
  export function experiencesCredentialsPath() {
@@ -197,7 +197,7 @@ const AGENT_BINARIES = {
197
197
  // Default to small/fast models for single-component classification — cheap and accurate enough.
198
198
  const DEFAULT_MODELS = {
199
199
  claude: 'haiku',
200
- codex: 'gpt-4.1-nano', // requires OPENAI_API_KEY; ChatGPT account users must pass --model
200
+ codex: 'gpt-5.4-mini', // requires OPENAI_API_KEY; ChatGPT account users must pass --model
201
201
  opencode: 'claude-haiku-4-5',
202
202
  cursor: 'claude-3-5-haiku-20241022',
203
203
  };
@@ -225,8 +225,11 @@ export async function runAgent(options) {
225
225
  const args = buildArgs(agent, prompt, model);
226
226
  return new Promise((resolve) => {
227
227
  const child = spawn(binary, args, {
228
- stdio: interactive ? 'inherit' : ['ignore', 'pipe', 'pipe'],
228
+ stdio: interactive ? 'inherit' : ['pipe', 'pipe', 'pipe'],
229
229
  });
230
+ if (!interactive) {
231
+ child.stdin?.end();
232
+ }
230
233
  let stdout = '';
231
234
  let stderr = '';
232
235
  let timedOut = false;
@@ -11,6 +11,7 @@ import { GenerateView } from './tui/GenerateView.js';
11
11
  import { registerGenerateEditCommand } from './edit/command.js';
12
12
  import { openPipelineDb, loadRawComponents, applyToolCalls, applyTokenToolCalls, computeComponentInputHash, computeTokenInputHash, lookupCache, lookupCacheByEntity, storeCache, copyComponentFromCache, copyTokensFromCache, renameEmptySlots, } from '../session/db.js';
13
13
  import { getRefineArtifactsRoot, getRefineSessionPaths } from '../analyze/select/persistence.js';
14
+ import { readExperiencesCredentials } from '../credentials-store.js';
14
15
  const execFileAsync = promisify(execFile);
15
16
  const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
16
17
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
@@ -292,10 +293,13 @@ async function loadAcceptedNames(sessionId) {
292
293
  }
293
294
  }
294
295
  async function runGenerateSkill(skill, opts, verbose = false) {
295
- if (!VALID_AGENTS.has(opts.agent)) {
296
- die(`Error: unknown agent '${opts.agent}'. Accepted values: claude, codex, opencode, cursor`);
296
+ const savedCreds = await readExperiencesCredentials();
297
+ const agentName = opts.agent ?? savedCreds.agent;
298
+ const model = opts.model ?? savedCreds.agentModel;
299
+ if (!agentName || !VALID_AGENTS.has(agentName)) {
300
+ die(`Error: no agent configured. Pass --agent <name> or run experiences setup. Accepted values: claude, codex, opencode, cursor`);
297
301
  }
298
- const agent = opts.agent;
302
+ const agent = agentName;
299
303
  if (skill === 'tokens' && !opts.rawTokens) {
300
304
  die('Error: --raw-tokens is required when using generate tokens');
301
305
  }
@@ -386,7 +390,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
386
390
  const db = openPipelineDb();
387
391
  let componentResults;
388
392
  try {
389
- componentResults = await runAllComponents(agent, opts.model, db, sessionId, allComponents, tokensInline, tokenMapInline, verbose, opts.cache === false || process.env.EDS_NO_CACHE === '1');
393
+ componentResults = await runAllComponents(agent, model, db, sessionId, allComponents, tokensInline, tokenMapInline, verbose, opts.cache === false || process.env.EDS_NO_CACHE === '1');
390
394
  }
391
395
  finally {
392
396
  db.close();
@@ -478,7 +482,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
478
482
  });
479
483
  const result = await runAgent({
480
484
  agent,
481
- model: opts.model,
485
+ model,
482
486
  prompt,
483
487
  interactive: false,
484
488
  timeoutMs: DEFAULT_TIMEOUT_MS * 5,
@@ -533,7 +537,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
533
537
  }
534
538
  function addAgentFlags(cmd) {
535
539
  return cmd
536
- .requiredOption('--agent <name>', 'Agent to use: claude, codex, opencode, cursor')
540
+ .option('--agent <name>', 'Agent to use: claude, codex, opencode, cursor (defaults to value saved by experiences setup)')
537
541
  .option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
538
542
  .option('--verbose', 'Show full agent output including reasoning text')
539
543
  .option('--dry-run', 'Print the prompt without invoking the agent')
@@ -329,28 +329,80 @@ async function setupBuild(repoRoot) {
329
329
  return true;
330
330
  }
331
331
  // ── Step 4: agent CLI ─────────────────────────────────────────────────────────
332
+ const AGENT_DEFS = [
333
+ { name: 'Claude Code', binary: 'claude', installHint: 'npm install -g @anthropic-ai/claude-code && claude login' },
334
+ { name: 'OpenAI Codex', binary: 'codex', installHint: 'npm install -g @openai/codex (requires OPENAI_API_KEY)' },
335
+ { name: 'OpenCode', binary: 'opencode', installHint: 'npm install -g opencode-ai && opencode auth' },
336
+ ];
337
+ function pick(items, defaultIdx = 0) {
338
+ items.forEach((item, i) => {
339
+ const num = `\x1b[1m[${i + 1}]\x1b[0m`;
340
+ const desc = item.description ? ` \x1b[2m${item.description}\x1b[0m` : '';
341
+ const def = i === defaultIdx ? ` \x1b[2m(default)\x1b[0m` : '';
342
+ process.stdout.write(` ${num} ${item.label}${desc}${def}\n`);
343
+ });
344
+ process.stdout.write(` \x1b[2m[s] Skip\x1b[0m\n`);
345
+ }
346
+ async function promptCodexModel() {
347
+ if (process.env['OPENAI_API_KEY'])
348
+ return undefined; // API key users get the default (gpt-4.1-nano)
349
+ info('');
350
+ process.stdout.write(` \x1b[33m⚠\x1b[0m No OPENAI_API_KEY — using ChatGPT account authentication.\n`);
351
+ info(' Tip: run \x1b[1mcodex\x1b[0m then type \x1b[1m/model\x1b[0m to browse all available models.');
352
+ info('');
353
+ info(' Choose a model:');
354
+ info('');
355
+ pick([
356
+ { label: 'gpt-5.4-mini', description: 'fast, lower cost' },
357
+ { label: 'gpt-5.5', description: 'most capable' },
358
+ { label: 'gpt-5.4' },
359
+ ]);
360
+ info('');
361
+ const choice = await prompt(' \x1b[2m›\x1b[0m Your choice [1]: ');
362
+ if (choice === '1' || choice === '')
363
+ return 'gpt-5.4-mini';
364
+ if (choice === '2')
365
+ return 'gpt-5.5';
366
+ if (choice === '3')
367
+ return 'gpt-5.4';
368
+ return undefined;
369
+ }
332
370
  async function setupAgent() {
333
- section('Step 4: Coding agent (claude, codex, opencode, or cursor)', '[required]');
371
+ section('Step 4: Coding agent (claude, codex, or opencode)', '[required]');
334
372
  info('experiences import uses a coding agent to generate component definitions.');
335
373
  info('');
336
- const agents = [
337
- { name: 'Claude Code', binary: 'claude', installHint: 'npm install -g @anthropic-ai/claude-code && claude login' },
338
- { name: 'OpenAI Codex', binary: 'codex', installHint: 'npm install -g @openai/codex (requires OPENAI_API_KEY)' },
339
- { name: 'OpenCode', binary: 'opencode', installHint: 'npm install -g opencode-ai && opencode auth' },
340
- ];
341
- for (const agent of agents) {
342
- if (await binaryExists(agent.binary)) {
343
- ok(`${agent.name} (${agent.binary}) found`);
344
- return true;
345
- }
374
+ const found = (await Promise.all(AGENT_DEFS.map(async (a) => ((await binaryExists(a.binary)) ? a : null)))).filter((a) => a !== null);
375
+ if (found.length === 1) {
376
+ ok(`${found[0].name} (${found[0].binary}) found`);
377
+ const agentModel = found[0].binary === 'codex' ? await promptCodexModel() : undefined;
378
+ return { agent: found[0].binary, agentModel };
379
+ }
380
+ if (found.length > 1) {
381
+ info('Multiple coding agents found. Choose one to use as the default:');
382
+ info('');
383
+ pick(found.map((a) => ({ label: `${a.name}`, description: a.binary })));
384
+ info('');
385
+ const choice = await prompt(' \x1b[2m›\x1b[0m Your choice [1]: ');
386
+ if (choice.toLowerCase() === 's') {
387
+ warn('Skipped. Install a coding agent before running experiences import.');
388
+ return { agent: undefined, agentModel: undefined };
389
+ }
390
+ const parsed = choice === '' ? 1 : parseInt(choice, 10);
391
+ const idx = Number.isNaN(parsed) || parsed < 1 || parsed > found.length ? 0 : parsed - 1;
392
+ const selected = found[idx];
393
+ ok(`${selected.name} \x1b[2m(${selected.binary})\x1b[0m selected`);
394
+ const agentModel = selected.binary === 'codex' ? await promptCodexModel() : undefined;
395
+ return { agent: selected.binary, agentModel };
346
396
  }
347
397
  warn('No coding agent found on PATH');
348
398
  info('');
349
399
  info('Choose one to install:');
350
- info(' [1] Claude Code (recommended) — npm install -g @anthropic-ai/claude-code');
351
- info(' [2] OpenAI Codex — npm install -g @openai/codex');
352
- info(' [3] OpenCode — npm install -g opencode-ai');
353
- info(' [s] Skip for now');
400
+ info('');
401
+ pick([
402
+ { label: 'Claude Code', description: 'npm install -g @anthropic-ai/claude-code' },
403
+ { label: 'OpenAI Codex', description: 'npm install -g @openai/codex' },
404
+ { label: 'OpenCode', description: 'npm install -g opencode-ai' },
405
+ ]);
354
406
  info('');
355
407
  const choice = await prompt(' Your choice: ');
356
408
  if (choice === '1' || choice === '') {
@@ -358,48 +410,48 @@ async function setupAgent() {
358
410
  if (r.exitCode !== 0) {
359
411
  fail('Install failed');
360
412
  info(r.stderr.trim().split('\n').slice(0, 5).join('\n'));
361
- return false;
413
+ return { agent: undefined, agentModel: undefined };
362
414
  }
363
415
  if (!(await binaryExists('claude'))) {
364
416
  fail('claude binary not found on PATH after install — check your npm global bin directory');
365
- return false;
417
+ return { agent: undefined, agentModel: undefined };
366
418
  }
367
419
  ok('Claude Code installed');
368
420
  info('');
369
421
  info('Next: run `claude login` to authenticate (browser OAuth).');
370
422
  info('Or set ANTHROPIC_API_KEY in your shell profile.');
371
- return true;
423
+ return { agent: 'claude', agentModel: undefined };
372
424
  }
373
425
  if (choice === '2') {
374
426
  const r = await runSpawn('npm', ['install', '-g', '@openai/codex']);
375
427
  if (r.exitCode !== 0) {
376
428
  fail('Install failed');
377
- return false;
429
+ return { agent: undefined, agentModel: undefined };
378
430
  }
379
431
  if (!(await binaryExists('codex'))) {
380
432
  fail('codex binary not found on PATH after install — check your npm global bin directory');
381
- return false;
433
+ return { agent: undefined, agentModel: undefined };
382
434
  }
383
435
  ok('OpenAI Codex installed');
384
- info('Set OPENAI_API_KEY in your shell profile to authenticate.');
385
- return true;
436
+ const agentModel = await promptCodexModel();
437
+ return { agent: 'codex', agentModel };
386
438
  }
387
439
  if (choice === '3') {
388
440
  const r = await runSpawn('npm', ['install', '-g', 'opencode-ai']);
389
441
  if (r.exitCode !== 0) {
390
442
  fail('Install failed');
391
- return false;
443
+ return { agent: undefined, agentModel: undefined };
392
444
  }
393
445
  if (!(await binaryExists('opencode'))) {
394
446
  fail('opencode binary not found on PATH after install — check your npm global bin directory');
395
- return false;
447
+ return { agent: undefined, agentModel: undefined };
396
448
  }
397
449
  ok('OpenCode installed');
398
450
  info('Run `opencode auth` to configure your provider.');
399
- return true;
451
+ return { agent: 'opencode', agentModel: undefined };
400
452
  }
401
453
  warn('Skipped. Install a coding agent before running experiences import.');
402
- return false;
454
+ return { agent: undefined, agentModel: undefined };
403
455
  }
404
456
  // ── Step 5: Contentful credentials ───────────────────────────────────────────
405
457
  async function setupContentfulCredentials() {
@@ -462,7 +514,8 @@ async function setupContentfulCredentials() {
462
514
  }
463
515
  const hostInput = await prompt(` API host [${currentHost}]: `);
464
516
  const host = toConfiguredHost(hostInput) ?? storedHost;
465
- await writeExperiencesCredentials({ spaceId, environmentId, cmaToken, ...(host ? { host } : {}) });
517
+ const existing = await readExperiencesCredentials();
518
+ await writeExperiencesCredentials({ ...existing, spaceId, environmentId, cmaToken, ...(host ? { host } : {}) });
466
519
  ok(`Credentials saved to ${experiencesCredentialsPath()}`);
467
520
  ok(`API host set to ${host ?? DEFAULT_CONFIGURED_HOST}`);
468
521
  info('Run experiences import — credentials will be pre-filled automatically.');
@@ -612,14 +665,27 @@ async function checkBuild(pkgRoot) {
612
665
  }
613
666
  async function checkAgent() {
614
667
  section('Checking coding agent');
615
- const agents = [
616
- { name: 'Claude Code', binary: 'claude' },
617
- { name: 'OpenAI Codex', binary: 'codex' },
618
- { name: 'OpenCode', binary: 'opencode' },
619
- ];
668
+ const agents = AGENT_DEFS;
669
+ const creds = await readExperiencesCredentials();
670
+ const savedAgent = creds.agent;
671
+ const savedModel = creds.agentModel;
672
+ if (savedAgent) {
673
+ const found = await binaryExists(savedAgent);
674
+ if (found) {
675
+ const modelStr = savedModel ? ` — model: ${savedModel}` : '';
676
+ ok(`${savedAgent}${modelStr} (saved preference)`);
677
+ return true;
678
+ }
679
+ else {
680
+ warn(`Saved agent '${savedAgent}' not found on PATH`);
681
+ info(`Re-run experiences setup to reconfigure.`);
682
+ return false;
683
+ }
684
+ }
620
685
  for (const agent of agents) {
621
686
  if (await binaryExists(agent.binary)) {
622
687
  ok(`${agent.name} (${agent.binary}) found`);
688
+ info('Tip: run experiences setup to save a default agent and model.');
623
689
  return true;
624
690
  }
625
691
  }
@@ -704,7 +770,6 @@ export function registerSetupCommand(program) {
704
770
  .action(async (opts) => {
705
771
  process.stdout.write('\n\x1b[1mexperiences setup\x1b[0m — interactive setup wizard\n');
706
772
  process.stdout.write('Sets up everything you need to run \x1b[1mexperiences import\x1b[0m.\n');
707
- process.stdout.write('Required steps are marked \x1b[31m[required]\x1b[0m, optional ones \x1b[2m[optional]\x1b[0m.\n');
708
773
  const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
709
774
  const repoRoot = join(pkgRoot, '..', '..');
710
775
  const profilePath = await detectShellProfile();
@@ -729,8 +794,13 @@ export function registerSetupCommand(program) {
729
794
  }
730
795
  // Step 4: agent
731
796
  if (!opts.skipAgent) {
732
- const agentOk = await setupAgent();
733
- results.push({ name: 'coding agent', passed: agentOk, required: true });
797
+ const { agent, agentModel } = await setupAgent();
798
+ if (agent) {
799
+ const stored = await readExperiencesCredentials();
800
+ const { agentModel: _staleModel, ...storedWithoutModel } = stored;
801
+ await writeExperiencesCredentials({ ...storedWithoutModel, agent, ...(agentModel ? { agentModel } : {}) });
802
+ }
803
+ results.push({ name: 'coding agent', passed: !!agent, required: true });
734
804
  }
735
805
  else {
736
806
  info('\nSkipping agent check (--skip-agent)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.11.0",
3
+ "version": "2.11.1-dev-build-96af281.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "react-dom": "^18.3.1",
37
37
  "ts-morph": "^27.0.2",
38
38
  "typescript": "^5.9.3",
39
- "@contentful/experience-design-system-types": "2.11.0"
39
+ "@contentful/experience-design-system-types": "2.11.1-dev-build-96af281.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@tsconfig/node24": "^24.0.3",