@contentful/experience-design-system-cli 2.12.1-dev-build-bae15c7.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 +25 -37
- package/dist/src/import/orchestrator.js +15 -2
- package/dist/src/import/picker-dispatch.d.ts +26 -0
- package/dist/src/import/picker-dispatch.js +44 -0
- 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
package/dist/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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']
|
|
14
|
-
environmentId: process.env['CONTENTFUL_ENVIRONMENT_ID']
|
|
15
|
-
cmaToken: process.env['CONTENTFUL_MANAGEMENT_TOKEN']
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
319
|
+
child.on('close', (code) => resolve(code === 0));
|
|
285
320
|
});
|
|
286
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,8 +5,10 @@ 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';
|
|
11
|
+
import { dispatchPickerSelection } from './picker-dispatch.js';
|
|
10
12
|
export function registerImportCommand(program) {
|
|
11
13
|
program
|
|
12
14
|
.command('import')
|
|
@@ -220,15 +222,20 @@ export function registerImportCommand(program) {
|
|
|
220
222
|
isTTY: !!process.stdin.isTTY,
|
|
221
223
|
});
|
|
222
224
|
let pickerSelection = null;
|
|
225
|
+
// Ink `unmount` handle captured after `render(...)` below so the
|
|
226
|
+
// picker callback can tear down the wizard cleanly. Prior to this
|
|
227
|
+
// fix the callback did `setImmediate(() => process.exit(0))`, which
|
|
228
|
+
// killed the process before `dispatchPickerSelection` could run.
|
|
229
|
+
let unmountInk = null;
|
|
223
230
|
const pickerProps = {};
|
|
224
231
|
if (pickerDecision.shouldShow) {
|
|
225
232
|
pickerProps.initialRuns = pickerDecision.runs;
|
|
226
233
|
pickerProps.onRunPicked = (selection) => {
|
|
227
234
|
pickerSelection = selection;
|
|
228
|
-
|
|
235
|
+
unmountInk?.();
|
|
229
236
|
};
|
|
230
237
|
}
|
|
231
|
-
const { waitUntilExit } = render(createElement(WizardApp, {
|
|
238
|
+
const { waitUntilExit, unmount } = render(createElement(WizardApp, {
|
|
232
239
|
initialSpaceId: creds.spaceId,
|
|
233
240
|
initialEnvironmentId: creds.environmentId || 'master',
|
|
234
241
|
initialCmaToken: creds.cmaToken,
|
|
@@ -250,43 +257,24 @@ export function registerImportCommand(program) {
|
|
|
250
257
|
...(opts.rawTokens ? { initialRawTokensPath: resolve(opts.rawTokens) } : {}),
|
|
251
258
|
...pickerProps,
|
|
252
259
|
}));
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
/* Ink throws on process.exit; swallow so picker dispatch can run. */
|
|
258
|
-
}
|
|
260
|
+
unmountInk = unmount;
|
|
261
|
+
await waitUntilExit();
|
|
259
262
|
// ── Picker dispatch ─────────────────────────────────────────────
|
|
260
|
-
// If the operator picked a run, route into
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// resolveRunTarget and threads its session IDs through
|
|
264
|
-
// launchModifyWizard (see runs/replay-helpers.ts).
|
|
263
|
+
// If the operator picked a run, route into replayRun / modifyRun.
|
|
264
|
+
// dispatchPickerSelection lives in ./picker-dispatch.ts so this
|
|
265
|
+
// decision is testable without an Ink runtime.
|
|
265
266
|
if (pickerSelection) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (sel.action === 'modify' && sel.runId) {
|
|
280
|
-
await modifyRun({
|
|
281
|
-
runIdOrPath: sel.runId,
|
|
282
|
-
...(opts.outDir ? { outDir: opts.outDir } : {}),
|
|
283
|
-
...(opts.overwrite ? { overwrite: true } : {}),
|
|
284
|
-
...(opts.saveAsNew ? { saveAsNew: true } : {}),
|
|
285
|
-
...(opts.force ? { force: true } : {}),
|
|
286
|
-
});
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
// action === 'new' falls through — the wizard already advanced.
|
|
267
|
+
await dispatchPickerSelection(pickerSelection, {
|
|
268
|
+
...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
|
|
269
|
+
...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
|
|
270
|
+
...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
|
|
271
|
+
...(opts.host ? { host: opts.host } : {}),
|
|
272
|
+
interactive: !!process.stdout.isTTY,
|
|
273
|
+
...(opts.outDir ? { outDir: opts.outDir } : {}),
|
|
274
|
+
...(opts.overwrite ? { overwrite: true } : {}),
|
|
275
|
+
...(opts.saveAsNew ? { saveAsNew: true } : {}),
|
|
276
|
+
...(opts.force ? { force: true } : {}),
|
|
277
|
+
}, { replayRun, modifyRun, pickerPushRun });
|
|
290
278
|
}
|
|
291
279
|
return;
|
|
292
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RunPickerSelection } from '../runs/run-picker.js';
|
|
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';
|
|
4
|
+
export type PickerDispatchOptions = {
|
|
5
|
+
spaceId?: string;
|
|
6
|
+
environmentId?: string;
|
|
7
|
+
cmaToken?: string;
|
|
8
|
+
host?: string;
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
outDir?: string;
|
|
11
|
+
overwrite?: boolean;
|
|
12
|
+
saveAsNew?: boolean;
|
|
13
|
+
force?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type PickerDispatchDeps = {
|
|
16
|
+
replayRun: typeof replayRunFn;
|
|
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;
|
|
25
|
+
};
|
|
26
|
+
export declare function dispatchPickerSelection(selection: RunPickerSelection, opts: PickerDispatchOptions, deps: PickerDispatchDeps): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
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).
|
|
6
|
+
export async function dispatchPickerSelection(selection, opts, deps) {
|
|
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
|
+
}
|
|
22
|
+
await deps.replayRun({
|
|
23
|
+
runIdOrPath: selection.runId,
|
|
24
|
+
...(opts.spaceId ? { spaceId: opts.spaceId } : {}),
|
|
25
|
+
...(opts.environmentId ? { environmentId: opts.environmentId } : {}),
|
|
26
|
+
...(opts.cmaToken ? { cmaToken: opts.cmaToken } : {}),
|
|
27
|
+
...(opts.host ? { host: opts.host } : {}),
|
|
28
|
+
...(opts.interactive !== undefined ? { interactive: opts.interactive } : {}),
|
|
29
|
+
...(opts.force ? { force: true } : {}),
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (selection.action === 'modify' && selection.runId) {
|
|
34
|
+
await deps.modifyRun({
|
|
35
|
+
runIdOrPath: selection.runId,
|
|
36
|
+
...(opts.outDir ? { outDir: opts.outDir } : {}),
|
|
37
|
+
...(opts.overwrite ? { overwrite: true } : {}),
|
|
38
|
+
...(opts.saveAsNew ? { saveAsNew: true } : {}),
|
|
39
|
+
...(opts.force ? { force: true } : {}),
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// action === 'new' → no-op; the wizard already advanced past the picker.
|
|
44
|
+
}
|
|
@@ -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;
|