@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
@@ -0,0 +1,229 @@
1
+ import { mkdirSync, appendFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ // ── Redaction ────────────────────────────────────────────────────────────────
5
+ // Blocklist of secret-shaped keys; matched case-insensitively.
6
+ const SECRET_KEY_PATTERNS = [
7
+ /token/i,
8
+ /secret/i,
9
+ /password/i,
10
+ /authorization/i,
11
+ /^auth$/i,
12
+ /apikey/i,
13
+ /api_key/i,
14
+ /credential/i,
15
+ /bearer/i,
16
+ /^cma$/i,
17
+ /cmaToken/i,
18
+ ];
19
+ // Value-side patterns for strings that look like tokens even under innocuous keys.
20
+ const SECRET_VALUE_PATTERNS = [
21
+ /^Bearer\s+\S+/i,
22
+ /CFPAT-[A-Za-z0-9_-]{20,}/,
23
+ /sk-[A-Za-z0-9_-]{20,}/,
24
+ /xox[baprs]-[A-Za-z0-9-]{10,}/,
25
+ ];
26
+ const REDACTED = '«redacted»';
27
+ const MAX_STRING_LEN = 4000;
28
+ function redactValue(value, seen) {
29
+ if (value === null || value === undefined)
30
+ return value;
31
+ if (typeof value === 'string') {
32
+ if (SECRET_VALUE_PATTERNS.some((r) => r.test(value)))
33
+ return REDACTED;
34
+ return value.length > MAX_STRING_LEN
35
+ ? value.slice(0, MAX_STRING_LEN) + `…«+${value.length - MAX_STRING_LEN}»`
36
+ : value;
37
+ }
38
+ if (typeof value !== 'object')
39
+ return value;
40
+ if (seen.has(value))
41
+ return '«cycle»';
42
+ seen.add(value);
43
+ if (Array.isArray(value))
44
+ return value.map((v) => redactValue(v, seen));
45
+ const out = {};
46
+ for (const [k, v] of Object.entries(value)) {
47
+ if (SECRET_KEY_PATTERNS.some((r) => r.test(k))) {
48
+ out[k] = REDACTED;
49
+ }
50
+ else {
51
+ out[k] = redactValue(v, seen);
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ export function redactForDebug(payload) {
57
+ return redactValue(payload, new WeakSet());
58
+ }
59
+ // ── Path resolution ──────────────────────────────────────────────────────────
60
+ const DEBUG_ROOT_ENV = 'EDSI_DEBUG_ROOT';
61
+ const DEBUG_LOG_ENV = 'EDSI_DEBUG_LOG';
62
+ function defaultDebugRoot() {
63
+ return process.env[DEBUG_ROOT_ENV] ?? join(homedir(), '.contentful', 'experience-design-system-cli', 'debug');
64
+ }
65
+ // Session timestamp source. Deterministic-friendly: honors EDSI_DEBUG_TS for tests.
66
+ function makeSessionTimestamp() {
67
+ const override = process.env['EDSI_DEBUG_TS'];
68
+ if (override)
69
+ return override;
70
+ const now = new Date();
71
+ const pad = (n, w = 2) => n.toString().padStart(w, '0');
72
+ return (`${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}` +
73
+ `T${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}Z`);
74
+ }
75
+ // ── Singleton ───────────────────────────────────────────────────────────────
76
+ let singleton = null;
77
+ class NoopDebugLogger {
78
+ enabled = false;
79
+ path = null;
80
+ event() {
81
+ /* no-op */
82
+ }
83
+ }
84
+ class FileDebugLogger {
85
+ enabled = true;
86
+ path;
87
+ sessionStart;
88
+ constructor(path) {
89
+ this.path = path;
90
+ this.sessionStart = Date.now();
91
+ mkdirSync(dirname(path), { recursive: true });
92
+ // Header event — records the process context.
93
+ this.write({
94
+ ts: new Date().toISOString(),
95
+ category: 'config',
96
+ name: 'session.open',
97
+ pid: process.pid,
98
+ ppid: process.ppid,
99
+ cwd: process.cwd(),
100
+ argv: process.argv.slice(1),
101
+ node: process.version,
102
+ platform: process.platform,
103
+ });
104
+ }
105
+ event(category, name, payload = {}) {
106
+ const redacted = redactForDebug(payload);
107
+ this.write({
108
+ ts: new Date().toISOString(),
109
+ elapsedMs: Date.now() - this.sessionStart,
110
+ category,
111
+ name,
112
+ pid: process.pid,
113
+ ...redacted,
114
+ });
115
+ }
116
+ write(record) {
117
+ try {
118
+ appendFileSync(this.path, JSON.stringify(record) + '\n', { encoding: 'utf8' });
119
+ }
120
+ catch {
121
+ // Debug logging must never crash the CLI. Swallow.
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Initialize the process-wide debug logger. Safe to call multiple times —
127
+ * subsequent calls return the existing instance.
128
+ *
129
+ * If EDSI_DEBUG_LOG is already set in the environment, this process joins
130
+ * that log file (used by spawned subprocesses so a whole `experiences import`
131
+ * flow lands in a single file).
132
+ */
133
+ export function initDebugLogger(opts) {
134
+ if (singleton)
135
+ return singleton;
136
+ // Inherited from parent process — always join the existing file, regardless
137
+ // of the local flag/env decision. Parent already made the choice.
138
+ const inherited = process.env[DEBUG_LOG_ENV];
139
+ if (inherited) {
140
+ singleton = new FileDebugLogger(inherited);
141
+ if (opts.command)
142
+ singleton.event('config', 'subprocess.start', { command: opts.command });
143
+ return singleton;
144
+ }
145
+ if (!opts.enabled) {
146
+ singleton = new NoopDebugLogger();
147
+ return singleton;
148
+ }
149
+ const root = opts.root ?? defaultDebugRoot();
150
+ const ts = makeSessionTimestamp();
151
+ const suffix = opts.command ? `-${opts.command}` : '';
152
+ const path = join(root, `${ts}${suffix}.jsonl`);
153
+ singleton = new FileDebugLogger(path);
154
+ process.env[DEBUG_LOG_ENV] = path; // propagate to spawned children
155
+ if (opts.command)
156
+ singleton.event('config', 'command.start', { command: opts.command });
157
+ // Emit a close event on process exit. Best-effort — writeFileSync-based, so
158
+ // it works from within an 'exit' handler.
159
+ process.on('exit', (code) => {
160
+ if (singleton && singleton.enabled) {
161
+ singleton.event('config', 'session.close', { exitCode: code });
162
+ }
163
+ });
164
+ return singleton;
165
+ }
166
+ export function getDebugLogger() {
167
+ return singleton ?? new NoopDebugLogger();
168
+ }
169
+ /** Test-only: reset the singleton. */
170
+ export function __resetDebugLoggerForTest() {
171
+ singleton = null;
172
+ delete process.env[DEBUG_LOG_ENV];
173
+ }
174
+ // ── Resolver ────────────────────────────────────────────────────────────────
175
+ /**
176
+ * Resolve effective debug-mode setting from three sources.
177
+ *
178
+ * Precedence (highest first):
179
+ * 1. `--debug` / `--no-debug` CLI flag
180
+ * 2. `EDSI_DEBUG` env var (truthy: 1, true, yes, on)
181
+ * 3. Persisted `debug` field in credentials.json
182
+ * 4. Default: OFF
183
+ */
184
+ export function resolveDebugMode(opts, configDebug) {
185
+ if (opts.debug !== undefined)
186
+ return opts.debug;
187
+ const env = process.env['EDSI_DEBUG'];
188
+ if (env !== undefined && env !== '') {
189
+ const v = env.toLowerCase();
190
+ if (v === '1' || v === 'true' || v === 'yes' || v === 'on')
191
+ return true;
192
+ if (v === '0' || v === 'false' || v === 'no' || v === 'off')
193
+ return false;
194
+ }
195
+ if (configDebug !== undefined)
196
+ return configDebug;
197
+ return false;
198
+ }
199
+ // ── Bright-green banner ─────────────────────────────────────────────────────
200
+ /**
201
+ * Emit the bright-green "debug logs at <path>" banner to stderr.
202
+ * No-op when debug is disabled or stderr is not a TTY-friendly stream.
203
+ * Callers should invoke this once at start and once at end of the command.
204
+ */
205
+ export function printDebugBanner(logger, phase) {
206
+ if (!logger.enabled || !logger.path)
207
+ return;
208
+ const prefix = phase === 'start' ? '\x1b[92m[debug]\x1b[0m ' : '\x1b[92m[debug]\x1b[0m ';
209
+ const label = phase === 'start' ? 'debug logs at' : 'debug logs written to';
210
+ // Bright green (92) + bold (1); reset with 0.
211
+ const line = `${prefix}\x1b[1m\x1b[92m${label} ${logger.path}\x1b[0m\n`;
212
+ try {
213
+ process.stderr.write(line);
214
+ }
215
+ catch {
216
+ /* ignore */
217
+ }
218
+ }
219
+ /** Directory that contains the current process's debug log, if any. */
220
+ export function debugLogPath() {
221
+ return singleton?.path ?? process.env[DEBUG_LOG_ENV] ?? null;
222
+ }
223
+ /** Ensure spawned children join the same debug log by including EDSI_DEBUG_LOG in their env. */
224
+ export function debugEnvForSubprocess(env = process.env) {
225
+ const path = debugLogPath();
226
+ if (!path)
227
+ return env;
228
+ return { ...env, [DEBUG_LOG_ENV]: path };
229
+ }
@@ -0,0 +1,14 @@
1
+ import { type DebugLogger } from './debug-logger.js';
2
+ /**
3
+ * Preamble every top-level command action should call before doing work.
4
+ *
5
+ * Resolves the effective debug decision (flag > env > persisted config),
6
+ * initializes the singleton, and prints the bright-green start banner.
7
+ * Registers a process-exit handler that prints the end banner once.
8
+ *
9
+ * Returns the logger so the caller can emit events without a second import.
10
+ * When debug is disabled, the returned logger is a no-op.
11
+ */
12
+ export declare function beginCommand(command: string, opts: {
13
+ debug?: boolean;
14
+ }): Promise<DebugLogger>;
@@ -0,0 +1,33 @@
1
+ import { initDebugLogger, printDebugBanner, resolveDebugMode } from './debug-logger.js';
2
+ import { readExperiencesCredentials } from '../credentials-store.js';
3
+ let endBannerRegistered = false;
4
+ /**
5
+ * Preamble every top-level command action should call before doing work.
6
+ *
7
+ * Resolves the effective debug decision (flag > env > persisted config),
8
+ * initializes the singleton, and prints the bright-green start banner.
9
+ * Registers a process-exit handler that prints the end banner once.
10
+ *
11
+ * Returns the logger so the caller can emit events without a second import.
12
+ * When debug is disabled, the returned logger is a no-op.
13
+ */
14
+ export async function beginCommand(command, opts) {
15
+ let configDebug;
16
+ try {
17
+ const creds = await readExperiencesCredentials();
18
+ configDebug = creds.debug;
19
+ }
20
+ catch {
21
+ // Missing credentials.json is fine — the resolver falls through to default OFF.
22
+ }
23
+ const enabled = resolveDebugMode(opts, configDebug);
24
+ const logger = initDebugLogger({ enabled, command });
25
+ if (logger.enabled) {
26
+ printDebugBanner(logger, 'start');
27
+ if (!endBannerRegistered) {
28
+ endBannerRegistered = true;
29
+ process.on('exit', () => printDebugBanner(logger, 'end'));
30
+ }
31
+ }
32
+ return logger;
33
+ }
@@ -11,6 +11,7 @@ import { registerPrintCommand } from './print/command.js';
11
11
  import { registerImportCommand } from './import/command.js';
12
12
  import { registerSetupCommand } from './setup/command.js';
13
13
  import { registerRunsCommand } from './runs/ls-command.js';
14
+ import { beginCommand } from './lib/debug-preamble.js';
14
15
  const require = createRequire(import.meta.url);
15
16
  const pkg = require('../package.json');
16
17
  /**
@@ -65,5 +66,30 @@ export function createProgram() {
65
66
  registerSetupCommand(program);
66
67
  registerRunsCommand(program);
67
68
  registerBuildCommand(program);
69
+ // Expose --debug on every subcommand. The flag is inherited automatically
70
+ // via `option()` at program scope + `preAction` reading merged opts from all
71
+ // ancestors. When set (or when EDSI_DEBUG / persisted config is on), the
72
+ // process-wide DebugLogger is initialized before the subcommand action runs
73
+ // and a bright-green "debug logs at <path>" banner is printed to stderr.
74
+ program.option('--debug', 'Write a JSONL trace of every decision to ~/.contentful/experience-design-system-cli/debug/');
75
+ program.option('--no-debug', 'Force debug logging off (overrides EDSI_DEBUG and persisted setup preference)');
76
+ program.hook('preAction', async (_thisCommand, actionCommand) => {
77
+ // Merge opts from actionCommand and all ancestors — root-level --debug
78
+ // set alongside a subcommand ends up on the root command's opts, not the
79
+ // subcommand's.
80
+ let debug;
81
+ for (let c = actionCommand; c; c = c.parent) {
82
+ const opts = c.opts();
83
+ if (opts.debug !== undefined) {
84
+ debug = opts.debug;
85
+ break;
86
+ }
87
+ }
88
+ // Build a `command` label out of the actual subcommand chain (e.g. "apply push").
89
+ const chain = [];
90
+ for (let c = actionCommand; c && c.parent; c = c.parent)
91
+ chain.unshift(c.name());
92
+ await beginCommand(chain.join(' ') || actionCommand.name(), { ...(debug !== undefined ? { debug } : {}) });
93
+ });
68
94
  return program;
69
95
  }
@@ -20,5 +20,7 @@ export type ModifyLauncherInput = {
20
20
  initialEnvironmentId?: string;
21
21
  /** Pre-fill host (from the run record's pushedTo). */
22
22
  initialHost?: string;
23
+ /** Pre-fill CMA token (from credentials.json / env). */
24
+ initialCmaToken?: string;
23
25
  };
24
26
  export declare function launchModifyWizard(input: ModifyLauncherInput): Promise<void>;
@@ -31,6 +31,8 @@ export async function launchModifyWizard(input) {
31
31
  props.initialEnvironmentId = input.initialEnvironmentId;
32
32
  if (input.initialHost)
33
33
  props.initialHost = input.initialHost;
34
+ if (input.initialCmaToken)
35
+ props.initialCmaToken = input.initialCmaToken;
34
36
  const { waitUntilExit } = render(createElement(WizardApp, props));
35
37
  await waitUntilExit();
36
38
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Push-from-picker launcher. When the operator selects "Push" from the
3
+ * interactive run-picker, we mount the wizard with seeded session IDs and
4
+ * `initialStep: 'push-from-picker'` so they see the same preview + push UX
5
+ * as a fresh `experiences import` (preview → diff review → pushing progress
6
+ * bars → done with view URL).
7
+ *
8
+ * This is deliberately parallel to `modify-launcher.ts`. The `--push-from-run`
9
+ * CLI flag path (headless shell-out via `replayRun`) is preserved for
10
+ * scripted / non-TTY use.
11
+ */
12
+ export type PushLauncherInput = {
13
+ extractSessionId: string;
14
+ generateSessionId: string | null;
15
+ /** Token session id from the run record; null when no tokens were
16
+ * generated. Forwarded to the wizard so preview picks up tokens too. */
17
+ tokenSessionId?: string | null;
18
+ projectPath: string;
19
+ savePath: string;
20
+ /** Absolute path to the run's tokens.json on disk (from the run record).
21
+ * Forwarded to the wizard so runPreview can hydrate DTCG tokens without
22
+ * re-emitting them to disk. Null when the run had no tokens. */
23
+ tokensPath?: string | null;
24
+ /** Pre-fill space id (from the run record's pushedTo or credentials.json). */
25
+ initialSpaceId?: string;
26
+ /** Pre-fill environment id (from the run record's pushedTo or credentials.json). */
27
+ initialEnvironmentId?: string;
28
+ /** Pre-fill host (from the run record's pushedTo or credentials.json). */
29
+ initialHost?: string;
30
+ /** Pre-fill CMA token (from credentials.json / env). */
31
+ initialCmaToken?: string;
32
+ };
33
+ export declare function launchPushWizard(input: PushLauncherInput): Promise<void>;
34
+ export type PickerPushRunOptions = {
35
+ runIdOrPath: string;
36
+ /** From `--space-id` flag. */
37
+ spaceId?: string;
38
+ /** From `--environment-id` flag. */
39
+ environmentId?: string;
40
+ /** From `--cma-token` flag. */
41
+ cmaToken?: string;
42
+ /** From `--host` flag. */
43
+ host?: string;
44
+ /** When true, bypass the source/saved-file staleness check. */
45
+ force?: boolean;
46
+ };
47
+ /**
48
+ * Interactive picker-Push entry point. Resolves the run record, applies the
49
+ * staleness gate, layers credentials (flag → pushedTo → credentials.json),
50
+ * and mounts the wizard via `launchPushWizard`.
51
+ */
52
+ export declare function pickerPushRun(opts: PickerPushRunOptions): Promise<void>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Push-from-picker launcher. When the operator selects "Push" from the
3
+ * interactive run-picker, we mount the wizard with seeded session IDs and
4
+ * `initialStep: 'push-from-picker'` so they see the same preview + push UX
5
+ * as a fresh `experiences import` (preview → diff review → pushing progress
6
+ * bars → done with view URL).
7
+ *
8
+ * This is deliberately parallel to `modify-launcher.ts`. The `--push-from-run`
9
+ * CLI flag path (headless shell-out via `replayRun`) is preserved for
10
+ * scripted / non-TTY use.
11
+ */
12
+ export async function launchPushWizard(input) {
13
+ const { render } = await import('ink');
14
+ const { createElement } = await import('react');
15
+ const { WizardApp } = await import('../import/tui/WizardApp.js');
16
+ const props = {
17
+ initialProjectPath: input.projectPath,
18
+ seedExtractSessionId: input.extractSessionId,
19
+ initialStep: 'push-from-picker',
20
+ };
21
+ if (input.generateSessionId)
22
+ props.seedGenerateSessionId = input.generateSessionId;
23
+ if (input.tokenSessionId)
24
+ props.seedTokenSessionId = input.tokenSessionId;
25
+ if (input.tokensPath)
26
+ props.seedTokensPath = input.tokensPath;
27
+ if (input.initialSpaceId)
28
+ props.initialSpaceId = input.initialSpaceId;
29
+ if (input.initialEnvironmentId)
30
+ props.initialEnvironmentId = input.initialEnvironmentId;
31
+ if (input.initialHost)
32
+ props.initialHost = input.initialHost;
33
+ if (input.initialCmaToken)
34
+ props.initialCmaToken = input.initialCmaToken;
35
+ const { waitUntilExit } = render(createElement(WizardApp, props));
36
+ await waitUntilExit();
37
+ }
38
+ /**
39
+ * Interactive picker-Push entry point. Resolves the run record, applies the
40
+ * staleness gate, layers credentials (flag → pushedTo → credentials.json),
41
+ * and mounts the wizard via `launchPushWizard`.
42
+ */
43
+ export async function pickerPushRun(opts) {
44
+ const { resolveRunTarget } = await import('./resolve-run-target.js');
45
+ const { readExperiencesCredentials } = await import('../credentials-store.js');
46
+ const { checkRunStaleness, formatStalenessDetail } = await import('./staleness.js');
47
+ const run = await resolveRunTarget(opts.runIdOrPath);
48
+ if (!opts.force) {
49
+ const staleness = await checkRunStaleness(run);
50
+ if (staleness.stale) {
51
+ const detail = formatStalenessDetail(staleness);
52
+ throw new Error([
53
+ `Refusing to push run ${run.id} — source or saved files have drifted since the run was recorded.`,
54
+ ...detail,
55
+ '',
56
+ 'Re-extract with `experiences import --project <path>` for a fresh run, or pass --force to bypass.',
57
+ ].join('\n'));
58
+ }
59
+ }
60
+ const stored = await readExperiencesCredentials();
61
+ const spaceId = opts.spaceId || run.pushedTo?.spaceId || stored.spaceId || '';
62
+ const environmentId = opts.environmentId || run.pushedTo?.environmentId || stored.environmentId || '';
63
+ const cmaToken = opts.cmaToken || stored.cmaToken || '';
64
+ const host = opts.host || run.pushedTo?.host || stored.host || '';
65
+ await launchPushWizard({
66
+ extractSessionId: run.extractSessionId,
67
+ generateSessionId: run.generateSessionId,
68
+ tokenSessionId: run.tokenSessionId,
69
+ projectPath: run.projectPath,
70
+ savePath: run.savePath,
71
+ tokensPath: run.tokensPath,
72
+ ...(spaceId ? { initialSpaceId: spaceId } : {}),
73
+ ...(environmentId ? { initialEnvironmentId: environmentId } : {}),
74
+ ...(host ? { initialHost: host } : {}),
75
+ ...(cmaToken ? { initialCmaToken: cmaToken } : {}),
76
+ });
77
+ }
@@ -128,6 +128,21 @@ export async function modifyRun(opts) {
128
128
  }
129
129
  }
130
130
  const saveMode = opts.overwrite ? 'overwrite' : opts.saveAsNew ? 'new' : 'prompt';
131
+ // Credentials pre-fill precedence: run.pushedTo field-by-field, then
132
+ // credentials.json (which itself falls back to env vars). The modify entry
133
+ // step goes straight to final-review and does not surface the credentials
134
+ // step, so if `pushedTo` is null (last push failed or never happened) we
135
+ // must consult the on-disk store here — otherwise the wizard mounts with
136
+ // built-in defaults (environmentId='master', host='api.contentful.com')
137
+ // and preview fires against the wrong endpoint. CMA token is now included
138
+ // in the fallback because credentials-store.ts persists it (INTEG-4410);
139
+ // if it's still empty after the merge, the wizard's final-review preview
140
+ // will 401 and the operator sees a real gap — out of scope for this fix.
141
+ const stored = await readExperiencesCredentials();
142
+ const mergedSpaceId = run.pushedTo?.spaceId || stored.spaceId || '';
143
+ const mergedEnvironmentId = run.pushedTo?.environmentId || stored.environmentId || '';
144
+ const mergedHost = run.pushedTo?.host || stored.host || '';
145
+ const mergedCmaToken = stored.cmaToken || '';
131
146
  await launchModifyWizard({
132
147
  extractSessionId: run.extractSessionId,
133
148
  generateSessionId: run.generateSessionId,
@@ -137,12 +152,9 @@ export async function modifyRun(opts) {
137
152
  entryStep: 'final-review',
138
153
  saveMode,
139
154
  ...(opts.outDir ? { outDirOverride: resolve(opts.outDir) } : {}),
140
- // Pre-fill credentials from the run record's last push so the operator
141
- // doesn't have to re-type space/environment/host on modify. The CMA
142
- // token is never persisted, so it still resolves via env var /
143
- // credentials.json / interactive prompt at the credentials step.
144
- ...(run.pushedTo?.spaceId ? { initialSpaceId: run.pushedTo.spaceId } : {}),
145
- ...(run.pushedTo?.environmentId ? { initialEnvironmentId: run.pushedTo.environmentId } : {}),
146
- ...(run.pushedTo?.host ? { initialHost: run.pushedTo.host } : {}),
155
+ ...(mergedSpaceId ? { initialSpaceId: mergedSpaceId } : {}),
156
+ ...(mergedEnvironmentId ? { initialEnvironmentId: mergedEnvironmentId } : {}),
157
+ ...(mergedHost ? { initialHost: mergedHost } : {}),
158
+ ...(mergedCmaToken ? { initialCmaToken: mergedCmaToken } : {}),
147
159
  });
148
160
  }
@@ -7,6 +7,7 @@ import { createInterface } from 'node:readline';
7
7
  import { promisify } from 'node:util';
8
8
  import { readExperiencesCredentials, writeExperiencesCredentials, experiencesCredentialsPath, } from '../credentials-store.js';
9
9
  import { promptAutoFilterPreference } from './auto-filter-prompt.js';
10
+ import { promptDebugModePreference } from './debug-mode-prompt.js';
10
11
  import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
11
12
  const execFileAsync = promisify(execFile);
12
13
  const REQUIRED_NODE_MAJOR = 24;
@@ -459,6 +460,20 @@ async function setupContentfulCredentials() {
459
460
  section('Step 5: Contentful credentials', '[optional]');
460
461
  info(`Saved to ${experiencesCredentialsPath()} — loaded automatically by experiences import.`);
461
462
  info('');
463
+ // INTEG-4410: as of the precedence flip, disk wins over env — but env vars
464
+ // still act as a fallback when disk is empty, so operators who have any
465
+ // CONTENTFUL_* / EDS_HOST env set should know the ambient value would take
466
+ // effect if they skip this step. Warn regardless of the choice.
467
+ const envShadowing = [
468
+ process.env['CONTENTFUL_SPACE_ID'] ? 'CONTENTFUL_SPACE_ID' : null,
469
+ process.env['CONTENTFUL_ENVIRONMENT_ID'] ? 'CONTENTFUL_ENVIRONMENT_ID' : null,
470
+ process.env['CONTENTFUL_MANAGEMENT_TOKEN'] ? 'CONTENTFUL_MANAGEMENT_TOKEN' : null,
471
+ process.env['EDS_HOST'] ? 'EDS_HOST' : null,
472
+ ].filter((v) => !!v);
473
+ if (envShadowing.length > 0) {
474
+ warn(`Env vars set: ${envShadowing.join(', ')}. Values saved here take precedence; env vars only apply where disk is empty.`);
475
+ info('');
476
+ }
462
477
  const stored = await readExperiencesCredentials();
463
478
  const currentSpace = stored.spaceId;
464
479
  const currentEnv = stored.environmentId;
@@ -599,7 +614,22 @@ async function setupQoL(profilePath) {
599
614
  else {
600
615
  dim(' skipped');
601
616
  }
602
- // 6d: NO_COLOR
617
+ // 6d: Debug-mode default
618
+ info('');
619
+ info('Debug logging — writes a JSONL trace of every command decision (agent calls, tool calls,');
620
+ info('apply actions, filter decisions, etc.) to ~/.contentful/experience-design-system-cli/debug/.');
621
+ info('Useful for developers debugging the CLI; OFF by default because traces are verbose.');
622
+ const debugCreds = await readExperiencesCredentials();
623
+ const debugChoice = await promptDebugModePreference((q) => prompt(q), debugCreds.debug);
624
+ if (debugChoice !== (debugCreds.debug ?? false)) {
625
+ await writeExperiencesCredentials({ ...debugCreds, debug: debugChoice });
626
+ ok(`Debug logging default set to ${debugChoice ? 'ON' : 'OFF'}`);
627
+ }
628
+ else {
629
+ dim(' unchanged');
630
+ }
631
+ info('');
632
+ // 6e: NO_COLOR
603
633
  info('');
604
634
  info('NO_COLOR — set to 1 to disable ANSI color output (useful in CI or plain terminals).');
605
635
  const setNoColor = await confirm('Add NO_COLOR=1 (disable colors) to your profile?', false);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ask the operator whether to enable debug logging by default.
3
+ *
4
+ * When ON, every command writes a JSONL trace of its decisions to
5
+ * ~/.contentful/experience-design-system-cli/debug/. Off by default because
6
+ * the trace file grows quickly and contains verbose per-tool-call detail
7
+ * intended for developers debugging the CLI.
8
+ *
9
+ * Injectable `ask` so tests can drive without a TTY.
10
+ */
11
+ export declare function promptDebugModePreference(ask: (q: string) => Promise<string>, current?: boolean): Promise<boolean>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Ask the operator whether to enable debug logging by default.
3
+ *
4
+ * When ON, every command writes a JSONL trace of its decisions to
5
+ * ~/.contentful/experience-design-system-cli/debug/. Off by default because
6
+ * the trace file grows quickly and contains verbose per-tool-call detail
7
+ * intended for developers debugging the CLI.
8
+ *
9
+ * Injectable `ask` so tests can drive without a TTY.
10
+ */
11
+ export async function promptDebugModePreference(ask, current) {
12
+ const defaultValue = current ?? false;
13
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
14
+ const answer = (await ask(` Enable debug logging by default? ${hint} `)).trim().toLowerCase();
15
+ if (answer === '')
16
+ return defaultValue;
17
+ if (answer.startsWith('y'))
18
+ return true;
19
+ if (answer.startsWith('n'))
20
+ return false;
21
+ return defaultValue;
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.12.1-dev-build-0b7de73.0",
3
+ "version": "2.12.1",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "svelte": "^5.56.4",
38
38
  "ts-morph": "^27.0.2",
39
39
  "typescript": "^5.9.3",
40
- "@contentful/experience-design-system-types": "2.12.1-dev-build-0b7de73.0"
40
+ "@contentful/experience-design-system-types": "2.12.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tsconfig/node24": "^24.0.3",