@contentful/experience-design-system-cli 2.12.1-dev-build-0b7de73.0 → 2.12.1-dev-build-c47114b.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 +1 -1
- package/dist/src/apply/api-client.js +45 -4
- package/dist/src/apply/command.js +1 -12
- package/dist/src/apply/preview-utils.d.ts +12 -0
- package/dist/src/apply/preview-utils.js +22 -0
- package/dist/src/credentials-store.d.ts +16 -0
- package/dist/src/credentials-store.js +22 -5
- package/dist/src/generate/agent-runner.js +40 -5
- package/dist/src/generate/command.js +2 -2
- package/dist/src/import/auto-filter-resolve.d.ts +0 -9
- package/dist/src/import/auto-filter-resolve.js +5 -5
- package/dist/src/import/command.js +2 -1
- package/dist/src/import/orchestrator.js +15 -2
- package/dist/src/import/picker-dispatch.d.ts +8 -0
- package/dist/src/import/picker-dispatch.js +19 -4
- package/dist/src/import/tui/WizardApp.d.ts +8 -2
- package/dist/src/import/tui/WizardApp.js +86 -19
- package/dist/src/import/tui/final-review-host.d.ts +8 -1
- package/dist/src/import/tui/final-review-host.js +2 -2
- package/dist/src/import/tui/push-progress.d.ts +0 -14
- package/dist/src/import/tui/push-progress.js +1 -14
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.js +16 -3
- package/dist/src/import/tui/steps/PushingStep.d.ts +2 -3
- package/dist/src/import/tui/steps/PushingStep.js +3 -20
- package/dist/src/lib/debug-logger.d.ts +49 -0
- package/dist/src/lib/debug-logger.js +229 -0
- package/dist/src/lib/debug-preamble.d.ts +14 -0
- package/dist/src/lib/debug-preamble.js +33 -0
- package/dist/src/program.js +26 -0
- package/dist/src/runs/modify-launcher.d.ts +2 -0
- package/dist/src/runs/modify-launcher.js +2 -0
- package/dist/src/runs/push-launcher.d.ts +52 -0
- package/dist/src/runs/push-launcher.js +77 -0
- package/dist/src/runs/replay-helpers.js +19 -7
- package/dist/src/setup/command.js +31 -1
- package/dist/src/setup/debug-mode-prompt.d.ts +11 -0
- package/dist/src/setup/debug-mode-prompt.js +22 -0
- 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
|
+
}
|
package/dist/src/program.js
CHANGED
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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:
|
|
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-
|
|
3
|
+
"version": "2.12.1-dev-build-c47114b.0",
|
|
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-
|
|
40
|
+
"@contentful/experience-design-system-types": "2.12.1-dev-build-c47114b.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@tsconfig/node24": "^24.0.3",
|