@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
package/dist/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",
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_API_HOST, toApiHost } from '../host-utils.js';
2
+ import { getDebugLogger } from '../lib/debug-logger.js';
2
3
  export const DEFAULT_HOST = DEFAULT_API_HOST;
3
4
  // Phase-prefix constants used at the two ApiError throw sites below and
4
5
  // imported by orchestrator.ts to identify preview-phase 422s for retry.
@@ -128,27 +129,57 @@ export class ImportApiClient {
128
129
  }
129
130
  async previewImport(manifest) {
130
131
  const url = `${this.base()}/design_systems/imports/preview`;
132
+ const debug = getDebugLogger();
133
+ const startedAt = Date.now();
134
+ debug.event('apply', 'preview.request', {
135
+ url,
136
+ componentCount: manifest.components?.length ?? 0,
137
+ tokenCount: manifest.designTokens?.length ?? 0,
138
+ });
131
139
  const res = await fetch(url, {
132
140
  method: 'POST',
133
141
  headers: this.headers(),
134
142
  body: JSON.stringify(manifest),
135
143
  });
136
144
  if (!res.ok) {
137
- throw new ApiError(`${PREVIEW_ERROR_PREFIX} ${res.status}`, res.status, await res.text());
145
+ const body = await res.text();
146
+ debug.event('apply', 'preview.error', {
147
+ status: res.status,
148
+ durationMs: Date.now() - startedAt,
149
+ bodyHead: body.slice(0, 2000),
150
+ });
151
+ throw new ApiError(`${PREVIEW_ERROR_PREFIX} ${res.status}`, res.status, body);
138
152
  }
139
- return (await res.json());
153
+ const parsed = (await res.json());
154
+ debug.event('apply', 'preview.ok', { status: res.status, durationMs: Date.now() - startedAt });
155
+ return parsed;
140
156
  }
141
157
  async applyImport(manifest, acknowledgeBreakingChanges) {
142
158
  const url = `${this.base()}/design_systems/imports/apply`;
159
+ const debug = getDebugLogger();
160
+ const startedAt = Date.now();
161
+ debug.event('apply', 'apply.request', { url, acknowledgeBreakingChanges });
143
162
  const res = await fetch(url, {
144
163
  method: 'POST',
145
164
  headers: this.headers(),
146
165
  body: JSON.stringify({ ...manifest, acknowledgeBreakingChanges }),
147
166
  });
148
167
  if (!res.ok) {
149
- throw new ApiError(`${APPLY_ERROR_PREFIX} ${res.status}`, res.status, await res.text());
168
+ const body = await res.text();
169
+ debug.event('apply', 'apply.error', {
170
+ status: res.status,
171
+ durationMs: Date.now() - startedAt,
172
+ bodyHead: body.slice(0, 2000),
173
+ });
174
+ throw new ApiError(`${APPLY_ERROR_PREFIX} ${res.status}`, res.status, body);
150
175
  }
151
- return (await res.json());
176
+ const parsed = (await res.json());
177
+ debug.event('apply', 'apply.accepted', {
178
+ status: res.status,
179
+ operationId: parsed.sys?.id,
180
+ durationMs: Date.now() - startedAt,
181
+ });
182
+ return parsed;
152
183
  }
153
184
  async pollOperation(operationId, opts = {}) {
154
185
  const intervalMs = opts.intervalMs ?? 2000;
@@ -166,7 +197,17 @@ export class ImportApiClient {
166
197
  }
167
198
  const op = (await res.json());
168
199
  opts.onProgress?.(op);
200
+ getDebugLogger().event('apply', 'poll.tick', {
201
+ operationId,
202
+ attempt,
203
+ status: op.sys.status,
204
+ });
169
205
  if (terminalStatuses.has(op.sys.status)) {
206
+ getDebugLogger().event('apply', 'poll.terminal', {
207
+ operationId,
208
+ attempt,
209
+ status: op.sys.status,
210
+ });
170
211
  return op;
171
212
  }
172
213
  if (attempt < maxAttempts - 1) {
@@ -5,6 +5,7 @@ import { join } from 'node:path';
5
5
  import { validateCDF, flattenDTCG, validateDTCG, buildManifest, buildFilteredManifest, } from '@contentful/experience-design-system-types';
6
6
  import { ApiError, ImportApiClient } from './api-client.js';
7
7
  import { openPipelineDb, loadCDFComponents } from '../session/db.js';
8
+ import { isEmptyPreview } from './preview-utils.js';
8
9
  import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
9
10
  import { SelectView, makeSelectKey } from './tui/SelectView.js';
10
11
  import { buildPostPushUrl } from '../lib/contentful-urls.js';
@@ -170,18 +171,6 @@ async function resolveSharedInputs(opts) {
170
171
  return { components, tokens, client };
171
172
  }
172
173
  // --- Output helpers ---
173
- function isEmptyPreview(preview) {
174
- const { components, tokens, taxonomies } = preview;
175
- return (components.new.length === 0 &&
176
- components.changed.length === 0 &&
177
- components.removed.length === 0 &&
178
- tokens.new.length === 0 &&
179
- tokens.changed.length === 0 &&
180
- tokens.removed.length === 0 &&
181
- taxonomies.new.length === 0 &&
182
- taxonomies.changed.length === 0 &&
183
- taxonomies.removed.length === 0);
184
- }
185
174
  export function hasBreakingChangesWithImpact(preview) {
186
175
  const allChanged = [...preview.components.changed, ...preview.tokens.changed];
187
176
  return allChanged.some((c) => c.changeClassification?.classification === 'breaking' &&
@@ -0,0 +1,12 @@
1
+ import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
2
+ /**
3
+ * True when a preview response describes zero server-side changes across
4
+ * every diff bucket (components, tokens, taxonomies). Used by:
5
+ * - `experiences apply` (CLI): short-circuit the confirm-and-push step.
6
+ * - `experiences import` wizard: block finalize when the resulting push
7
+ * would be a pure no-op (INTEG-4411 refined guard).
8
+ *
9
+ * A push that produces ANY entry in ANY bucket — including a rejection that
10
+ * removes a server-side component — is NOT empty.
11
+ */
12
+ export declare function isEmptyPreview(preview: ServerPreviewResponse): boolean;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * True when a preview response describes zero server-side changes across
3
+ * every diff bucket (components, tokens, taxonomies). Used by:
4
+ * - `experiences apply` (CLI): short-circuit the confirm-and-push step.
5
+ * - `experiences import` wizard: block finalize when the resulting push
6
+ * would be a pure no-op (INTEG-4411 refined guard).
7
+ *
8
+ * A push that produces ANY entry in ANY bucket — including a rejection that
9
+ * removes a server-side component — is NOT empty.
10
+ */
11
+ export function isEmptyPreview(preview) {
12
+ const { components, tokens, taxonomies } = preview;
13
+ return (components.new.length === 0 &&
14
+ components.changed.length === 0 &&
15
+ components.removed.length === 0 &&
16
+ tokens.new.length === 0 &&
17
+ tokens.changed.length === 0 &&
18
+ tokens.removed.length === 0 &&
19
+ taxonomies.new.length === 0 &&
20
+ taxonomies.changed.length === 0 &&
21
+ taxonomies.removed.length === 0);
22
+ }
@@ -10,7 +10,23 @@ export type ExperiencesCredentials = {
10
10
  /** Feature 8: persisted custom prompt path for `generate components`. */
11
11
  generatePromptPath?: string;
12
12
  autoFilter?: boolean;
13
+ /** Feature: default debug-mode (writes JSONL trace of every decision) for all commands. */
14
+ debug?: boolean;
13
15
  };
16
+ /**
17
+ * Read persisted Contentful credentials.
18
+ *
19
+ * Precedence (INTEG-4410): what the operator saved on disk via
20
+ * `experiences setup` or the wizard's credentials step wins over ambient
21
+ * `CONTENTFUL_*` / `EDS_HOST` env vars. Env vars are still consulted as a
22
+ * fallback when the field on disk is missing or empty — this preserves
23
+ * back-compat for CI / scripts that only export env and never call setup.
24
+ *
25
+ * The pre-INTEG-4410 order (env-first) silently shadowed saved values, so
26
+ * operators who saved a different space via setup kept seeing the env one
27
+ * pre-filled in the wizard. The saved value now wins; the env fallback only
28
+ * fires when the on-disk field is empty.
29
+ */
14
30
  export declare function readExperiencesCredentials(): Promise<ExperiencesCredentials>;
15
31
  export declare function writeExperiencesCredentials(creds: ExperiencesCredentials): Promise<void>;
16
32
  export declare function experiencesCredentialsPath(): string;
@@ -4,21 +4,37 @@ import { homedir } from 'node:os';
4
4
  import { toConfiguredHost } from './host-utils.js';
5
5
  const CREDENTIALS_DIR = join(homedir(), '.config', 'experiences');
6
6
  const CREDENTIALS_PATH = join(CREDENTIALS_DIR, 'credentials.json');
7
+ /**
8
+ * Read persisted Contentful credentials.
9
+ *
10
+ * Precedence (INTEG-4410): what the operator saved on disk via
11
+ * `experiences setup` or the wizard's credentials step wins over ambient
12
+ * `CONTENTFUL_*` / `EDS_HOST` env vars. Env vars are still consulted as a
13
+ * fallback when the field on disk is missing or empty — this preserves
14
+ * back-compat for CI / scripts that only export env and never call setup.
15
+ *
16
+ * The pre-INTEG-4410 order (env-first) silently shadowed saved values, so
17
+ * operators who saved a different space via setup kept seeing the env one
18
+ * pre-filled in the wizard. The saved value now wins; the env fallback only
19
+ * fires when the on-disk field is empty.
20
+ */
7
21
  export async function readExperiencesCredentials() {
8
22
  try {
9
23
  const raw = await readFile(CREDENTIALS_PATH, 'utf8');
10
24
  const parsed = JSON.parse(raw);
11
- const host = toConfiguredHost(process.env['EDS_HOST'] ?? parsed.host);
25
+ // Disk value wins when non-empty; env is the fallback.
26
+ const host = toConfiguredHost(parsed.host || process.env['EDS_HOST']);
12
27
  return {
13
- spaceId: process.env['CONTENTFUL_SPACE_ID'] ?? parsed.spaceId ?? '',
14
- environmentId: process.env['CONTENTFUL_ENVIRONMENT_ID'] ?? parsed.environmentId ?? '',
15
- cmaToken: process.env['CONTENTFUL_MANAGEMENT_TOKEN'] ?? parsed.cmaToken ?? '',
28
+ spaceId: parsed.spaceId || process.env['CONTENTFUL_SPACE_ID'] || '',
29
+ environmentId: parsed.environmentId || process.env['CONTENTFUL_ENVIRONMENT_ID'] || '',
30
+ cmaToken: parsed.cmaToken || process.env['CONTENTFUL_MANAGEMENT_TOKEN'] || '',
16
31
  ...(host ? { host } : {}),
17
32
  ...(parsed.agent ? { agent: parsed.agent } : {}),
18
33
  ...(parsed.agentModel ? { agentModel: parsed.agentModel } : {}),
19
34
  ...(parsed.selectPromptPath ? { selectPromptPath: parsed.selectPromptPath } : {}),
20
35
  ...(parsed.generatePromptPath ? { generatePromptPath: parsed.generatePromptPath } : {}),
21
36
  ...(typeof parsed.autoFilter === 'boolean' ? { autoFilter: parsed.autoFilter } : {}),
37
+ ...(typeof parsed.debug === 'boolean' ? { debug: parsed.debug } : {}),
22
38
  };
23
39
  }
24
40
  catch {
@@ -32,7 +48,7 @@ export async function readExperiencesCredentials() {
32
48
  }
33
49
  }
34
50
  export async function writeExperiencesCredentials(creds) {
35
- const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, ...rest } = creds;
51
+ const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, ...rest } = creds;
36
52
  const host = toConfiguredHost(creds.host);
37
53
  await mkdir(CREDENTIALS_DIR, { recursive: true });
38
54
  await writeFile(CREDENTIALS_PATH, JSON.stringify({
@@ -43,6 +59,7 @@ export async function writeExperiencesCredentials(creds) {
43
59
  ...(selectPromptPath ? { selectPromptPath } : {}),
44
60
  ...(generatePromptPath ? { generatePromptPath } : {}),
45
61
  ...(typeof autoFilter === 'boolean' ? { autoFilter } : {}),
62
+ ...(typeof debug === 'boolean' ? { debug } : {}),
46
63
  }, null, 2) + '\n', { mode: 0o600 });
47
64
  }
48
65
  export function experiencesCredentialsPath() {
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { getDebugLogger } from '../lib/debug-logger.js';
2
3
  const VALID_SELECT_TOOL_NAMES = new Set(['select_component', 'reject_component']);
3
4
  export function parseSelectToolCallLines(stdout) {
4
5
  const calls = [];
@@ -218,6 +219,10 @@ const DEFAULT_MODELS = {
218
219
  cursor: 'claude-3-5-haiku-20241022',
219
220
  };
220
221
  export function resolveBinary(agent) {
222
+ const envKey = `EDS_AGENT_BINARY_${agent.toUpperCase()}`;
223
+ const override = process.env[envKey];
224
+ if (override && override.trim())
225
+ return override.trim();
221
226
  return AGENT_BINARIES[agent];
222
227
  }
223
228
  function buildArgs(agent, prompt, model) {
@@ -239,6 +244,17 @@ export async function runAgent(options) {
239
244
  const { agent, prompt, interactive, timeoutMs, model, onOutput } = options;
240
245
  const binary = resolveBinary(agent);
241
246
  const args = buildArgs(agent, prompt, model);
247
+ const debug = getDebugLogger();
248
+ const startedAt = Date.now();
249
+ debug.event('agent', 'run.start', {
250
+ agent,
251
+ binary,
252
+ model,
253
+ interactive,
254
+ timeoutMs,
255
+ promptLen: prompt.length,
256
+ promptHead: prompt.slice(0, 500),
257
+ });
242
258
  return new Promise((resolve) => {
243
259
  const child = spawn(binary, args, {
244
260
  stdio: interactive ? 'inherit' : ['pipe', 'pipe', 'pipe'],
@@ -265,12 +281,24 @@ export async function runAgent(options) {
265
281
  }
266
282
  child.on('close', (code, signal) => {
267
283
  clearTimeout(timer);
268
- resolve({
284
+ const result = {
269
285
  exitCode: signal ? 1 : (code ?? 1),
270
286
  stdout,
271
287
  stderr,
272
288
  timedOut,
289
+ };
290
+ debug.event('agent', 'run.end', {
291
+ agent,
292
+ model,
293
+ durationMs: Date.now() - startedAt,
294
+ exitCode: result.exitCode,
295
+ signal,
296
+ timedOut,
297
+ stdoutLen: stdout.length,
298
+ stderrLen: stderr.length,
299
+ stderrTail: stderr.slice(-1000),
273
300
  });
301
+ resolve(result);
274
302
  });
275
303
  });
276
304
  }
@@ -278,12 +306,19 @@ export async function checkAgentAuth(agent) {
278
306
  if (agent !== 'claude')
279
307
  return 'ok';
280
308
  const binary = resolveBinary(agent);
281
- // Verify the binary exists first
282
- const whichResult = await new Promise((resolve) => {
309
+ // Verify the binary exists first. When `binary` is an absolute path (e.g.
310
+ // set via EDS_AGENT_BINARY_CLAUDE=/opt/custom/claude), `which` on some
311
+ // shells doesn't resolve it — check the filesystem directly for absolute
312
+ // paths, and fall back to `which` for bare names on $PATH.
313
+ const binaryExists = await new Promise((resolve) => {
314
+ if (binary.startsWith('/')) {
315
+ import('node:fs/promises').then((fs) => fs.access(binary).then(() => resolve(true), () => resolve(false)));
316
+ return;
317
+ }
283
318
  const child = spawn('which', [binary], { stdio: 'ignore' });
284
- child.on('close', (code) => resolve(code ?? 1));
319
+ child.on('close', (code) => resolve(code === 0));
285
320
  });
286
- if (whichResult !== 0)
321
+ if (!binaryExists)
287
322
  return 'not-found';
288
323
  // Use `claude auth status` — fast, no API call, works regardless of which
289
324
  // auth provider (direct, Bedrock, Vertex) or whether AWS_PROFILE is set.
@@ -496,7 +496,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
496
496
  await waitUntilExit();
497
497
  }
498
498
  else {
499
- process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession: ${sessionId ?? ''}\n`);
499
+ process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession=${sessionId ?? ''}\n`);
500
500
  process.exit(0);
501
501
  }
502
502
  return;
@@ -563,7 +563,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
563
563
  await waitUntilExit();
564
564
  }
565
565
  else {
566
- process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession: ${sessionId ?? ''}\n`);
566
+ process.stdout.write(`generate complete\nskill: ${skill}\nagent: ${agent}\nsession=${sessionId ?? ''}\n`);
567
567
  process.exit(0);
568
568
  }
569
569
  }
@@ -1,12 +1,3 @@
1
- /**
2
- * Resolve the effective auto-filter setting from a CLI flag plus the persisted
3
- * config preference.
4
- *
5
- * Precedence (highest first):
6
- * 1. CLI flag (`--auto-filter` or `--no-auto-filter`) — wins when set
7
- * 2. Config (`credentials.json` `autoFilter` field) — used when flag absent
8
- * 3. Default ON — when neither is set
9
- */
10
1
  export declare function resolveAutoFilter(opts: {
11
2
  autoFilter?: boolean;
12
3
  }, configAutoFilter?: boolean): boolean;
@@ -7,10 +7,10 @@
7
7
  * 2. Config (`credentials.json` `autoFilter` field) — used when flag absent
8
8
  * 3. Default ON — when neither is set
9
9
  */
10
+ import { getDebugLogger } from '../lib/debug-logger.js';
10
11
  export function resolveAutoFilter(opts, configAutoFilter) {
11
- if (opts.autoFilter !== undefined)
12
- return opts.autoFilter;
13
- if (configAutoFilter !== undefined)
14
- return configAutoFilter;
15
- return true;
12
+ const source = opts.autoFilter !== undefined ? 'flag' : configAutoFilter !== undefined ? 'config' : 'default';
13
+ const value = opts.autoFilter ?? configAutoFilter ?? true;
14
+ getDebugLogger().event('filter', 'auto-filter.resolve', { source, value });
15
+ return value;
16
16
  }
@@ -5,6 +5,7 @@ import { resolveAgent, resolveModel } from './agent-model-resolve.js';
5
5
  import { readExperiencesCredentials } from '../credentials-store.js';
6
6
  import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
7
7
  import { replayRun, modifyRun } from '../runs/replay-helpers.js';
8
+ import { pickerPushRun } from '../runs/push-launcher.js';
8
9
  import { resolvePromptFlags } from './print-prompt.js';
9
10
  import { shouldShowRunPicker } from '../runs/run-picker-mount.js';
10
11
  import { dispatchPickerSelection } from './picker-dispatch.js';
@@ -273,7 +274,7 @@ export function registerImportCommand(program) {
273
274
  ...(opts.overwrite ? { overwrite: true } : {}),
274
275
  ...(opts.saveAsNew ? { saveAsNew: true } : {}),
275
276
  ...(opts.force ? { force: true } : {}),
276
- }, { replayRun, modifyRun });
277
+ }, { replayRun, modifyRun, pickerPushRun });
277
278
  }
278
279
  return;
279
280
  }
@@ -5,13 +5,17 @@ import { execFile } from 'node:child_process';
5
5
  import { openPipelineDb, getOrCreateSession, createStep, updateStep, findLatestSessionForCommand, } from '../session/db.js';
6
6
  import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
7
7
  import { buildPostPushUrl } from '../lib/contentful-urls.js';
8
+ import { getDebugLogger, debugEnvForSubprocess } from '../lib/debug-logger.js';
8
9
  function findCliPath() {
9
10
  return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
10
11
  }
11
12
  async function runStep(args, cliPath, env = {}, streamStderr = false) {
13
+ const debug = getDebugLogger();
14
+ const startedAt = Date.now();
15
+ debug.event('import', 'subprocess.spawn', { cliPath, args });
12
16
  return new Promise((res) => {
13
17
  const child = execFile('node', [cliPath, ...args], {
14
- env: { ...process.env, ...env },
18
+ env: debugEnvForSubprocess({ ...process.env, ...env }),
15
19
  });
16
20
  let stdout = '';
17
21
  let stderr = '';
@@ -25,7 +29,16 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
25
29
  process.stderr.write(text);
26
30
  });
27
31
  child.on('close', (code) => {
28
- res({ exitCode: code ?? 0, stdout, stderr });
32
+ const exitCode = code ?? 0;
33
+ debug.event('import', 'subprocess.exit', {
34
+ args,
35
+ exitCode,
36
+ durationMs: Date.now() - startedAt,
37
+ stdoutLen: stdout.length,
38
+ stderrLen: stderr.length,
39
+ stderrTail: stderr.slice(-1000),
40
+ });
41
+ res({ exitCode, stdout, stderr });
29
42
  });
30
43
  });
31
44
  }
@@ -1,5 +1,6 @@
1
1
  import type { RunPickerSelection } from '../runs/run-picker.js';
2
2
  import type { replayRun as replayRunFn, modifyRun as modifyRunFn } from '../runs/replay-helpers.js';
3
+ import type { pickerPushRun as pickerPushRunFn } from '../runs/push-launcher.js';
3
4
  export type PickerDispatchOptions = {
4
5
  spaceId?: string;
5
6
  environmentId?: string;
@@ -14,5 +15,12 @@ export type PickerDispatchOptions = {
14
15
  export type PickerDispatchDeps = {
15
16
  replayRun: typeof replayRunFn;
16
17
  modifyRun: typeof modifyRunFn;
18
+ /**
19
+ * Interactive picker-Push launcher — mounts the wizard so the operator sees
20
+ * preview + push progress + view URL rather than a single-line summary.
21
+ * Optional so existing test fixtures that only stub {replayRun, modifyRun}
22
+ * still type-check; when omitted, picker-Push falls back to replayRun.
23
+ */
24
+ pickerPushRun?: typeof pickerPushRunFn;
17
25
  };
18
26
  export declare function dispatchPickerSelection(selection: RunPickerSelection, opts: PickerDispatchOptions, deps: PickerDispatchDeps): Promise<void>;
@@ -1,9 +1,24 @@
1
- // Route a resolved RunPickerSelection into replayRun / modifyRun. Extracted
2
- // from command.ts so the dispatch decision is testable without an Ink runtime
3
- // and so the picker callback in command.ts is a simple state-capture (no
4
- // process.exit shenanigans that would kill the process before dispatch runs).
1
+ // Route a resolved RunPickerSelection into replayRun / modifyRun / picker-push.
2
+ // Extracted from command.ts so the dispatch decision is testable without an
3
+ // Ink runtime and so the picker callback in command.ts is a simple
4
+ // state-capture (no process.exit shenanigans that would kill the process
5
+ // before dispatch runs).
5
6
  export async function dispatchPickerSelection(selection, opts, deps) {
6
7
  if (selection.action === 'push' && selection.runId) {
8
+ // Interactive TTY → mount the wizard's preview + push UX. Non-interactive
9
+ // (CI / scripted / non-TTY) → keep the headless shell-out via replayRun
10
+ // that `experiences import --push-from-run <id>` relies on.
11
+ if (opts.interactive !== false && deps.pickerPushRun) {
12
+ await deps.pickerPushRun({
13
+ runIdOrPath: selection.runId,
14
+ ...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
15
+ ...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
16
+ ...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
17
+ ...(opts.host ? { host: opts.host } : {}),
18
+ ...(opts.force ? { force: true } : {}),
19
+ });
20
+ return;
21
+ }
7
22
  await deps.replayRun({
8
23
  runIdOrPath: selection.runId,
9
24
  ...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
@@ -99,13 +99,19 @@ export type WizardAppProps = {
99
99
  * would never re-emit `tokens.json` for the modified save path.
100
100
  */
101
101
  seedTokenSessionId?: string;
102
+ /**
103
+ * Push-from-picker entry: overrides `state.tokensPath` so runPreview can
104
+ * read the run record's saved tokens.json without waiting for the wizard
105
+ * to re-emit it (push-from-picker skips the save flow entirely).
106
+ */
107
+ seedTokensPath?: string;
102
108
  /**
103
109
  * Modify-entry: overrides the wizard's initial step. When set, the wizard
104
110
  * bypasses its normal welcome/token-input bootstrap. Currently only
105
111
  * `'final-review'` is plumbed end-to-end; `'scope-gate'` is accepted for
106
112
  * future use but falls through to standard behavior.
107
113
  */
108
- initialStep?: 'scope-gate' | 'final-review';
114
+ initialStep?: 'scope-gate' | 'final-review' | 'push-from-picker';
109
115
  /**
110
116
  * Headless raw-token source path. When set (and the modify-entry props
111
117
  * are not), the wizard seeds `state.rawTokensPath` and lands directly on
@@ -125,4 +131,4 @@ export type WizardAppProps = {
125
131
  initialRuns?: RunRecord[];
126
132
  onRunPicked?: (selection: RunPickerSelection) => void;
127
133
  };
128
- export declare function WizardApp({ initialSpaceId, initialEnvironmentId, initialCmaToken, initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope, noCache, autoFilter, livePreview, noPush, noSave, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, initialStep, initialRawTokensPath, initialRuns, onRunPicked, }?: WizardAppProps): React.ReactElement;
134
+ export declare function WizardApp({ initialSpaceId, initialEnvironmentId, initialCmaToken, initialHost, initialAgent, initialModel, initialProjectPath, host, autoAcceptScope, noCache, autoFilter, livePreview, noPush, noSave, outDirOverride, onConflictMode, selectPromptPath, generatePromptPath, seedExtractSessionId, seedGenerateSessionId, seedTokenSessionId, seedTokensPath, initialStep, initialRawTokensPath, initialRuns, onRunPicked, }?: WizardAppProps): React.ReactElement;