@contentful/experience-design-system-cli 2.11.4-dev-build-d47ab65.0 → 2.12.1-dev-build-589b615.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.
Files changed (142) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +259 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +814 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  62. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  63. package/dist/src/import/tui/push-progress.d.ts +23 -0
  64. package/dist/src/import/tui/push-progress.js +14 -0
  65. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  66. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  67. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  68. package/dist/src/import/tui/run-teaser.js +9 -0
  69. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  70. package/dist/src/import/tui/runLivePreview.js +73 -0
  71. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  72. package/dist/src/import/tui/runScopeGate.js +28 -0
  73. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  74. package/dist/src/import/tui/scope-gate-host.js +20 -0
  75. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  76. package/dist/src/import/tui/spawn-generate.js +53 -0
  77. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  78. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  79. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  80. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  81. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  82. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  83. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  84. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  85. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  86. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  87. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  88. package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
  89. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  90. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  91. package/dist/src/import/tui/useLivePreview.js +120 -0
  92. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  93. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  94. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  95. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  96. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  97. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  98. package/dist/src/lib/contentful-urls.d.ts +34 -0
  99. package/dist/src/lib/contentful-urls.js +41 -0
  100. package/dist/src/program.js +2 -0
  101. package/dist/src/runs/export-helpers.d.ts +30 -0
  102. package/dist/src/runs/export-helpers.js +59 -0
  103. package/dist/src/runs/fingerprint.d.ts +47 -0
  104. package/dist/src/runs/fingerprint.js +75 -0
  105. package/dist/src/runs/ls-command.d.ts +12 -0
  106. package/dist/src/runs/ls-command.js +137 -0
  107. package/dist/src/runs/modify-launcher.d.ts +24 -0
  108. package/dist/src/runs/modify-launcher.js +36 -0
  109. package/dist/src/runs/path-prompt.d.ts +8 -0
  110. package/dist/src/runs/path-prompt.js +72 -0
  111. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  112. package/dist/src/runs/push-creds-prompt.js +39 -0
  113. package/dist/src/runs/push-helpers.d.ts +29 -0
  114. package/dist/src/runs/push-helpers.js +69 -0
  115. package/dist/src/runs/replay-helpers.d.ts +67 -0
  116. package/dist/src/runs/replay-helpers.js +148 -0
  117. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  118. package/dist/src/runs/resolve-run-target.js +45 -0
  119. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  120. package/dist/src/runs/run-picker-mount.js +72 -0
  121. package/dist/src/runs/run-picker.d.ts +17 -0
  122. package/dist/src/runs/run-picker.js +145 -0
  123. package/dist/src/runs/save-conflict.d.ts +8 -0
  124. package/dist/src/runs/save-conflict.js +56 -0
  125. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  126. package/dist/src/runs/save-path-resolver.js +71 -0
  127. package/dist/src/runs/staleness.d.ts +38 -0
  128. package/dist/src/runs/staleness.js +140 -0
  129. package/dist/src/runs/store.d.ts +78 -0
  130. package/dist/src/runs/store.js +136 -0
  131. package/dist/src/session/cache-keys.d.ts +15 -0
  132. package/dist/src/session/cache-keys.js +38 -0
  133. package/dist/src/session/db.d.ts +89 -3
  134. package/dist/src/session/db.js +447 -104
  135. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  136. package/dist/src/setup/auto-filter-prompt.js +24 -0
  137. package/dist/src/setup/command.d.ts +7 -0
  138. package/dist/src/setup/command.js +58 -2
  139. package/dist/src/types.d.ts +6 -0
  140. package/package.json +2 -2
  141. package/skills/generate-components.md +54 -6
  142. package/skills/select-components.md +13 -4
@@ -0,0 +1,137 @@
1
+ import { listRuns } from './store.js';
2
+ import { resolveRunTarget } from './resolve-run-target.js';
3
+ import { checkRunStaleness, shortStalenessSummary, formatStalenessDetail } from './staleness.js';
4
+ function pad(value, width) {
5
+ if (value.length >= width)
6
+ return value;
7
+ return value + ' '.repeat(width - value.length);
8
+ }
9
+ function formatCreated(iso) {
10
+ // Drop ms + Z, render as `YYYY-MM-DD HH:MM`.
11
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso);
12
+ return m ? `${m[1]} ${m[2]}` : iso;
13
+ }
14
+ function formatPushed(record) {
15
+ if (!record.pushedTo)
16
+ return '(not pushed)';
17
+ return `${record.pushedTo.spaceId}/${record.pushedTo.environmentId}`;
18
+ }
19
+ function makeColumns(staleness) {
20
+ return [
21
+ { header: 'ID', get: (r) => r.id },
22
+ { header: 'CREATED', get: (r) => formatCreated(r.createdAt) },
23
+ { header: 'PROJECT', get: (r) => r.projectPath },
24
+ { header: 'SAVED TO', get: (r) => r.savePath },
25
+ { header: 'COMPONENTS', get: (r) => String(r.componentCount) },
26
+ { header: 'PUSHED', get: formatPushed },
27
+ { header: 'STALE', get: (_r, i) => shortStalenessSummary(staleness[i]) },
28
+ ];
29
+ }
30
+ function renderFooter(runs, write) {
31
+ if (runs.length === 0)
32
+ return;
33
+ const id = runs[0].id;
34
+ write('\n');
35
+ write(`Push run ${id}: experiences import --push-from-run ${id}\n`);
36
+ write(`Modify run ${id}: experiences import --modify ${id}\n`);
37
+ }
38
+ async function renderDetail(run, write) {
39
+ const lines = [
40
+ `Run ${run.id}`,
41
+ `Created: ${formatCreated(run.createdAt)}`,
42
+ `Project: ${run.projectPath}`,
43
+ `Saved: ${run.savePath}`,
44
+ `Components: ${run.componentCount}`,
45
+ `Tokens: ${run.tokenCount}`,
46
+ ];
47
+ if (run.tokensPath) {
48
+ lines.push(`Tokens saved: ${run.tokensPath}`);
49
+ }
50
+ lines.push(`Agent: ${run.agent}`, `Pushed: ${formatPushed(run)}`, '');
51
+ // Staleness status block. v2/v1 records (no sourceFingerprint) render as
52
+ // UNKNOWN — the operator can re-extract for a fresh run.
53
+ if (!run.sourceFingerprint) {
54
+ lines.push('Status: UNKNOWN (run pre-dates the staleness fingerprint).');
55
+ }
56
+ else {
57
+ const staleness = await checkRunStaleness(run);
58
+ lines.push(...formatStalenessDetail(staleness));
59
+ }
60
+ lines.push('', `Push to Contentful: experiences import --push-from-run ${run.id}`, `Modify in wizard: experiences import --modify ${run.id}`, '');
61
+ write(lines.join('\n'));
62
+ }
63
+ export async function runLsCommand(opts = {}) {
64
+ const write = opts.write ?? ((s) => process.stdout.write(s));
65
+ if (opts.pushed && opts.notPushed) {
66
+ throw new Error('--pushed and --not-pushed are mutually exclusive');
67
+ }
68
+ if (opts.target) {
69
+ const run = await resolveRunTarget(opts.target);
70
+ if (opts.json) {
71
+ write(JSON.stringify(run, null, 2) + '\n');
72
+ return;
73
+ }
74
+ await renderDetail(run, write);
75
+ return;
76
+ }
77
+ let runs = await listRuns({
78
+ ...(opts.projectPath ? { projectPath: opts.projectPath } : {}),
79
+ ...(typeof opts.limit === 'number' ? { limit: opts.limit } : {}),
80
+ });
81
+ if (opts.pushed)
82
+ runs = runs.filter((r) => r.pushedTo !== null);
83
+ if (opts.notPushed)
84
+ runs = runs.filter((r) => r.pushedTo === null);
85
+ if (opts.json) {
86
+ write(JSON.stringify(runs, null, 2) + '\n');
87
+ return;
88
+ }
89
+ if (runs.length === 0) {
90
+ write('No runs recorded yet. Run `experiences import` to create one.\n');
91
+ return;
92
+ }
93
+ // Compute staleness once per row so the table is consistent across the
94
+ // STALE column and any future detail-equivalent surfaces.
95
+ const stalenessByIdx = await Promise.all(runs.map(async (r) => (r.sourceFingerprint ? checkRunStaleness(r) : null)));
96
+ const stalenessForCol = stalenessByIdx.map((s) => s ?? {
97
+ stale: false,
98
+ staleComponents: [],
99
+ staleTokens: false,
100
+ savedComponentsEdited: false,
101
+ savedTokensEdited: false,
102
+ missingSourceFiles: [],
103
+ });
104
+ const columns = makeColumns(stalenessForCol);
105
+ // Auto-expand each column to max(header, max(row value)) so long paths
106
+ // aren't silently truncated.
107
+ const widths = columns.map((c) => Math.max(c.header.length, ...runs.map((r, i) => c.get(r, i).length)));
108
+ const headerRow = columns.map((c, i) => pad(c.header, widths[i])).join(' ');
109
+ write(headerRow + '\n');
110
+ for (let i = 0; i < runs.length; i++) {
111
+ const r = runs[i];
112
+ const row = columns.map((c, ci) => pad(c.get(r, i), widths[ci])).join(' ');
113
+ write(row + '\n');
114
+ }
115
+ renderFooter(runs, write);
116
+ }
117
+ export function registerRunsCommand(program) {
118
+ program
119
+ .command('runs [target]')
120
+ .alias('ls')
121
+ .description('List recorded import runs from ~/.config/experiences/runs.json, or show one run when [target] (id or path) is given')
122
+ .option('--project <path>', 'Filter by source project path (absolute)')
123
+ .option('--limit <n>', 'Limit the number of rows', (v) => parseInt(v, 10))
124
+ .option('--json', 'Emit RunRecord JSON to stdout (array, or single object with [target])')
125
+ .option('--pushed', 'Only show runs that have been pushed to Contentful')
126
+ .option('--not-pushed', 'Only show runs that have not been pushed')
127
+ .action(async (target, options) => {
128
+ await runLsCommand({
129
+ ...(target ? { target } : {}),
130
+ ...(options.project ? { projectPath: options.project } : {}),
131
+ ...(typeof options.limit === 'number' && !Number.isNaN(options.limit) ? { limit: options.limit } : {}),
132
+ ...(options.json ? { json: true } : {}),
133
+ ...(options.pushed ? { pushed: true } : {}),
134
+ ...(options.notPushed ? { notPushed: true } : {}),
135
+ });
136
+ });
137
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Thin indirection so `runModifyCommand` can be unit-tested without spinning
3
+ * up the real Ink wizard. The production implementation lives here.
4
+ */
5
+ export type ModifyLauncherInput = {
6
+ extractSessionId: string;
7
+ generateSessionId: string | null;
8
+ /** Token session id from the run record; null when no tokens were
9
+ * generated. Forwarded to the wizard so the modify entry pre-loads
10
+ * tokens alongside the extract/generate sessions. */
11
+ tokenSessionId?: string | null;
12
+ projectPath: string;
13
+ savePath: string;
14
+ entryStep: 'scope-gate' | 'final-review';
15
+ saveMode: 'overwrite' | 'new' | 'prompt';
16
+ outDirOverride?: string;
17
+ /** Pre-fill space id (from the run record's pushedTo). */
18
+ initialSpaceId?: string;
19
+ /** Pre-fill environment id (from the run record's pushedTo). */
20
+ initialEnvironmentId?: string;
21
+ /** Pre-fill host (from the run record's pushedTo). */
22
+ initialHost?: string;
23
+ };
24
+ export declare function launchModifyWizard(input: ModifyLauncherInput): Promise<void>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Thin indirection so `runModifyCommand` can be unit-tested without spinning
3
+ * up the real Ink wizard. The production implementation lives here.
4
+ */
5
+ export async function launchModifyWizard(input) {
6
+ const { render } = await import('ink');
7
+ const { createElement } = await import('react');
8
+ const { WizardApp } = await import('../import/tui/WizardApp.js');
9
+ // Modify entry: re-open the wizard with the prior run's sessions seeded so
10
+ // extract + generate are skipped. The wizard short-circuits to `initialStep`
11
+ // (typically `final-review`) using state derived from the seed IDs. Saved
12
+ // credentials from the run record's `pushedTo` pre-fill the credentials
13
+ // step (CMA token is never persisted, so it still falls through to the
14
+ // env/credentials.json/prompt resolution path).
15
+ const props = {
16
+ initialProjectPath: input.projectPath,
17
+ seedExtractSessionId: input.extractSessionId,
18
+ initialStep: input.entryStep,
19
+ };
20
+ if (input.generateSessionId)
21
+ props.seedGenerateSessionId = input.generateSessionId;
22
+ if (input.tokenSessionId)
23
+ props.seedTokenSessionId = input.tokenSessionId;
24
+ if (input.saveMode === 'overwrite')
25
+ props.outDirOverride = input.savePath;
26
+ if (input.outDirOverride)
27
+ props.outDirOverride = input.outDirOverride;
28
+ if (input.initialSpaceId)
29
+ props.initialSpaceId = input.initialSpaceId;
30
+ if (input.initialEnvironmentId)
31
+ props.initialEnvironmentId = input.initialEnvironmentId;
32
+ if (input.initialHost)
33
+ props.initialHost = input.initialHost;
34
+ const { waitUntilExit } = render(createElement(WizardApp, props));
35
+ await waitUntilExit();
36
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export type PathPromptProps = {
3
+ defaultPath: string;
4
+ onSubmit: (path: string) => void;
5
+ onCancel: () => void;
6
+ label?: string;
7
+ };
8
+ export declare function PathPrompt({ defaultPath, onSubmit, onCancel, label, }: PathPromptProps): React.ReactElement;
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { readdirSync } from 'node:fs';
5
+ import { dirname, basename, join } from 'node:path';
6
+ import { useImmediateInput } from '../analyze/select/tui/hooks/useImmediateInput.js';
7
+ import { normalizePath } from '../import/path-utils.js';
8
+ function autocomplete(partial) {
9
+ if (!partial)
10
+ return null;
11
+ const normalized = normalizePath(partial);
12
+ const dir = dirname(normalized);
13
+ const base = basename(normalized);
14
+ let entries;
15
+ try {
16
+ entries = readdirSync(dir);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ const matches = entries.filter((e) => e.startsWith(base));
22
+ if (matches.length === 0)
23
+ return null;
24
+ if (matches.length === 1)
25
+ return join(dir, matches[0]);
26
+ // Common prefix
27
+ let prefix = matches[0];
28
+ for (const m of matches) {
29
+ while (!m.startsWith(prefix))
30
+ prefix = prefix.slice(0, -1);
31
+ if (!prefix)
32
+ break;
33
+ }
34
+ return prefix.length > base.length ? join(dir, prefix) : null;
35
+ }
36
+ export function PathPrompt({ defaultPath, onSubmit, onCancel, label = 'Save to', }) {
37
+ const [input, setInput] = useState('');
38
+ const [cursorVisible, setCursorVisible] = useState(true);
39
+ useEffect(() => {
40
+ const interval = setInterval(() => setCursorVisible((v) => !v), 500);
41
+ return () => clearInterval(interval);
42
+ }, []);
43
+ useImmediateInput((rawInput, key) => {
44
+ if (key.return) {
45
+ const trimmed = input.trim();
46
+ onSubmit(trimmed === '' ? defaultPath : normalizePath(trimmed));
47
+ return;
48
+ }
49
+ if (key.escape) {
50
+ onCancel();
51
+ return;
52
+ }
53
+ if (key.tab) {
54
+ const target = input.trim() || defaultPath;
55
+ const completed = autocomplete(target);
56
+ if (completed)
57
+ setInput(completed);
58
+ return;
59
+ }
60
+ if (key.backspace || key.delete) {
61
+ setInput((v) => v.slice(0, -1));
62
+ return;
63
+ }
64
+ if (rawInput && !key.ctrl && !key.meta) {
65
+ setInput((v) => v + rawInput);
66
+ }
67
+ });
68
+ const shown = input || defaultPath;
69
+ const isPlaceholder = input === '';
70
+ const cursor = cursorVisible ? '█' : ' ';
71
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "?" }), _jsxs(Text, { bold: true, children: [label, ":"] }), _jsx(Text, { dimColor: isPlaceholder, children: shown }), _jsx(Text, { children: cursor })] }), _jsxs(Box, { gap: 3, children: [_jsx(Text, { dimColor: true, children: "[Enter] Confirm" }), _jsx(Text, { dimColor: true, children: "[Tab] Autocomplete" }), _jsx(Text, { dimColor: true, children: "[Esc] Cancel" })] })] }));
72
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Interactive credentials fallback for `experiences import --push-from-run`.
3
+ *
4
+ * The wizard ships a full `CredentialsStep` Ink component already — rather
5
+ * than reimplementing the field-entry UI, we mount that step in a tiny Ink
6
+ * app that resolves a promise once the operator confirms.
7
+ *
8
+ * Returns the four credential values (host comes back as a configured host
9
+ * string, e.g. `api.contentful.com`).
10
+ */
11
+ export type CollectedCredentials = {
12
+ spaceId: string;
13
+ environmentId: string;
14
+ cmaToken: string;
15
+ host: string;
16
+ };
17
+ export type PromptForCredentialsOptions = {
18
+ initialSpaceId?: string;
19
+ initialEnvironmentId?: string;
20
+ initialCmaToken?: string;
21
+ initialHost?: string;
22
+ /** Short summary line rendered above the form. */
23
+ summary?: string;
24
+ };
25
+ export declare function promptForPushCredentials(opts?: PromptForCredentialsOptions): Promise<CollectedCredentials>;
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ export async function promptForPushCredentials(opts = {}) {
3
+ const { render, Box, Text } = await import('ink');
4
+ const { useState } = await import('react');
5
+ const { CredentialsStep } = await import('../import/tui/steps/CredentialsStep.js');
6
+ return new Promise((resolve, reject) => {
7
+ let app = null;
8
+ function App() {
9
+ const [done, setDone] = useState(false);
10
+ const handle = (spaceId, environmentId, cmaToken, host) => {
11
+ if (done)
12
+ return;
13
+ setDone(true);
14
+ // Defer unmount so Ink flushes the final render.
15
+ queueMicrotask(() => {
16
+ app?.unmount();
17
+ resolve({ spaceId, environmentId, cmaToken, host });
18
+ });
19
+ };
20
+ if (done) {
21
+ return React.createElement(Box, null, React.createElement(Text, null, ''));
22
+ }
23
+ return React.createElement(CredentialsStep, {
24
+ summary: opts.summary ?? 'Enter Contentful credentials to push this run. Press Enter on each field to advance.',
25
+ ...(opts.initialSpaceId !== undefined ? { initialSpaceId: opts.initialSpaceId } : {}),
26
+ ...(opts.initialEnvironmentId !== undefined ? { initialEnvironmentId: opts.initialEnvironmentId } : {}),
27
+ ...(opts.initialCmaToken !== undefined ? { initialCmaToken: opts.initialCmaToken } : {}),
28
+ ...(opts.initialHost !== undefined ? { initialHost: opts.initialHost } : {}),
29
+ onConfirm: handle,
30
+ onContinue: handle,
31
+ onQuit: () => {
32
+ app?.unmount();
33
+ reject(new Error('Credentials entry cancelled.'));
34
+ },
35
+ });
36
+ }
37
+ app = render(React.createElement(App));
38
+ });
39
+ }
@@ -0,0 +1,29 @@
1
+ export type PushSessionResult = {
2
+ ok: true;
3
+ } | {
4
+ ok: false;
5
+ error: string;
6
+ };
7
+ /**
8
+ * Resolve the package's CLI binary. Duplicates the walk from export-helpers
9
+ * so push-helpers can be imported independently.
10
+ */
11
+ declare function findCliPath(): string;
12
+ export type PushSessionOptions = {
13
+ sessionId: string;
14
+ spaceId: string;
15
+ environmentId: string;
16
+ cmaToken: string;
17
+ host?: string;
18
+ };
19
+ /**
20
+ * Push a recorded pipeline.db session's components + tokens to Contentful by
21
+ * shelling out to `experiences apply push --session <id>`. Does no local
22
+ * file I/O — that is the whole point of --push-from-run.
23
+ */
24
+ export declare function pushRunSession(opts: PushSessionOptions): Promise<PushSessionResult>;
25
+ /** Internal test surface. Not part of the public API. */
26
+ export declare const __testing: {
27
+ findCliPath: typeof findCliPath;
28
+ };
29
+ export {};
@@ -0,0 +1,69 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * Resolve the package's CLI binary. Duplicates the walk from export-helpers
7
+ * so push-helpers can be imported independently.
8
+ */
9
+ function findCliPath() {
10
+ let dir = dirname(fileURLToPath(import.meta.url));
11
+ for (let i = 0; i < 8; i++) {
12
+ const candidate = join(dir, 'bin', 'cli.js');
13
+ if (existsSync(candidate))
14
+ return candidate;
15
+ const parent = dirname(dir);
16
+ if (parent === dir)
17
+ break;
18
+ dir = parent;
19
+ }
20
+ return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
21
+ }
22
+ function runCli(args) {
23
+ const cliPath = findCliPath();
24
+ if (!existsSync(cliPath)) {
25
+ return Promise.resolve({
26
+ exitCode: 1,
27
+ stdout: '',
28
+ stderr: `Cannot find CLI binary at ${cliPath}`,
29
+ });
30
+ }
31
+ return new Promise((res) => {
32
+ execFile('node', [cliPath, ...args], (err, stdout, stderr) => {
33
+ res({
34
+ exitCode: err && 'code' in err && typeof err.code === 'number' ? err.code : err ? 1 : 0,
35
+ stdout,
36
+ stderr,
37
+ });
38
+ });
39
+ });
40
+ }
41
+ /**
42
+ * Push a recorded pipeline.db session's components + tokens to Contentful by
43
+ * shelling out to `experiences apply push --session <id>`. Does no local
44
+ * file I/O — that is the whole point of --push-from-run.
45
+ */
46
+ export async function pushRunSession(opts) {
47
+ const args = [
48
+ 'apply',
49
+ 'push',
50
+ '--session',
51
+ opts.sessionId,
52
+ '--space-id',
53
+ opts.spaceId,
54
+ '--environment-id',
55
+ opts.environmentId,
56
+ '--cma-token',
57
+ opts.cmaToken,
58
+ '--yes',
59
+ ];
60
+ if (opts.host) {
61
+ args.push('--host', opts.host);
62
+ }
63
+ const r = await runCli(args);
64
+ if (r.exitCode === 0)
65
+ return { ok: true };
66
+ return { ok: false, error: r.stderr.trim() || r.stdout.trim() || `exit ${r.exitCode}` };
67
+ }
68
+ /** Internal test surface. Not part of the public API. */
69
+ export const __testing = { findCliPath };
@@ -0,0 +1,67 @@
1
+ export type ReplayRunOptions = {
2
+ runIdOrPath: string;
3
+ /** From `--space-id` flag. */
4
+ spaceId?: string;
5
+ /** From `--environment-id` flag. */
6
+ environmentId?: string;
7
+ /** From `--cma-token` flag (or CONTENTFUL_MANAGEMENT_TOKEN env). */
8
+ cmaToken?: string;
9
+ /** From `--host` flag. */
10
+ host?: string;
11
+ /**
12
+ * When true (interactive TTY), prompt for missing credentials via the
13
+ * wizard's CredentialsStep. When false (non-TTY / CI), throw with the
14
+ * pinned error string from the spec.
15
+ */
16
+ interactive?: boolean;
17
+ /** When true, bypass the source/saved-file staleness check. */
18
+ force?: boolean;
19
+ /**
20
+ * Test seam: replace the interactive prompt with a deterministic resolver.
21
+ * The CLI surface never sets this; only used by tests.
22
+ */
23
+ promptForCredentials?: (need: {
24
+ spaceId?: string;
25
+ environmentId?: string;
26
+ cmaToken?: string;
27
+ host?: string;
28
+ }) => Promise<{
29
+ spaceId: string;
30
+ environmentId: string;
31
+ cmaToken: string;
32
+ host: string;
33
+ }>;
34
+ };
35
+ /**
36
+ * Push a prior run's recorded pipeline.db session directly to Contentful
37
+ * without re-running extraction or generation, and without writing any
38
+ * components.json / tokens.json to disk.
39
+ *
40
+ * Exposed as `experiences import --push-from-run <id-or-path>`.
41
+ *
42
+ * Credentials resolution order:
43
+ * 1. Flags on the command (`--space-id`, `--environment-id`, `--cma-token`,
44
+ * `--host`).
45
+ * 2. The run record's `pushedTo` field (space / env / host only — the
46
+ * CMA token is never persisted).
47
+ * 3. `~/.config/experiences/credentials.json` (the `experiences setup`
48
+ * store) and matching env vars.
49
+ * 4. If still incomplete and interactive: open the wizard's existing
50
+ * credentials step. If non-TTY: error.
51
+ */
52
+ export declare function replayRun(opts: ReplayRunOptions): Promise<void>;
53
+ export type ModifyRunOptions = {
54
+ runIdOrPath: string;
55
+ saveAsNew?: boolean;
56
+ overwrite?: boolean;
57
+ outDir?: string;
58
+ /** When true, bypass the source/saved-file staleness check. */
59
+ force?: boolean;
60
+ };
61
+ /**
62
+ * Re-open the wizard with a prior run's extract/generate session pre-populated
63
+ * so the operator can tweak fields. Exposed as
64
+ * `experiences import --modify <id-or-path>`. Default entry step is
65
+ * `final-review`.
66
+ */
67
+ export declare function modifyRun(opts: ModifyRunOptions): Promise<void>;
@@ -0,0 +1,148 @@
1
+ import { resolve } from 'node:path';
2
+ import { resolveRunTarget } from './resolve-run-target.js';
3
+ import { updateRun } from './store.js';
4
+ import { pushRunSession } from './push-helpers.js';
5
+ import { readExperiencesCredentials } from '../credentials-store.js';
6
+ import { launchModifyWizard } from './modify-launcher.js';
7
+ import { checkRunStaleness, formatStalenessDetail } from './staleness.js';
8
+ function formatStalenessRefusal(run, detail) {
9
+ return [
10
+ `Refusing to replay run ${run.id} — source or saved files have drifted since the run was recorded.`,
11
+ ...detail,
12
+ '',
13
+ 'Re-extract with `experiences import --project <path>` for a fresh run, or pass --force to bypass.',
14
+ ].join('\n');
15
+ }
16
+ const MISSING_CREDS_ERROR = "--push-from-run requires credentials (space-id / environment-id / cma-token). Pass --cma-token or run 'experiences setup' first.";
17
+ /**
18
+ * Push a prior run's recorded pipeline.db session directly to Contentful
19
+ * without re-running extraction or generation, and without writing any
20
+ * components.json / tokens.json to disk.
21
+ *
22
+ * Exposed as `experiences import --push-from-run <id-or-path>`.
23
+ *
24
+ * Credentials resolution order:
25
+ * 1. Flags on the command (`--space-id`, `--environment-id`, `--cma-token`,
26
+ * `--host`).
27
+ * 2. The run record's `pushedTo` field (space / env / host only — the
28
+ * CMA token is never persisted).
29
+ * 3. `~/.config/experiences/credentials.json` (the `experiences setup`
30
+ * store) and matching env vars.
31
+ * 4. If still incomplete and interactive: open the wizard's existing
32
+ * credentials step. If non-TTY: error.
33
+ */
34
+ export async function replayRun(opts) {
35
+ const run = await resolveRunTarget(opts.runIdOrPath);
36
+ // Staleness gate: refuse before any push side effect when source files or
37
+ // saved artifacts have drifted. Bypassed by --force.
38
+ if (!opts.force) {
39
+ const staleness = await checkRunStaleness(run);
40
+ if (staleness.stale) {
41
+ throw new Error(formatStalenessRefusal(run, formatStalenessDetail(staleness)));
42
+ }
43
+ }
44
+ const sessionId = run.generateSessionId ?? run.extractSessionId;
45
+ // Layered credentials resolution.
46
+ const stored = await readExperiencesCredentials();
47
+ let spaceId = opts.spaceId || run.pushedTo?.spaceId || stored.spaceId || '';
48
+ let environmentId = opts.environmentId || run.pushedTo?.environmentId || stored.environmentId || '';
49
+ let cmaToken = opts.cmaToken || stored.cmaToken || '';
50
+ let host = opts.host || run.pushedTo?.host || stored.host || '';
51
+ if (!spaceId || !environmentId || !cmaToken) {
52
+ if (!opts.interactive) {
53
+ throw new Error(MISSING_CREDS_ERROR);
54
+ }
55
+ const prompt = opts.promptForCredentials ??
56
+ (async (need) => {
57
+ const { promptForPushCredentials } = await import('./push-creds-prompt.js');
58
+ return promptForPushCredentials({
59
+ ...(need.spaceId ? { initialSpaceId: need.spaceId } : {}),
60
+ ...(need.environmentId ? { initialEnvironmentId: need.environmentId } : {}),
61
+ ...(need.cmaToken ? { initialCmaToken: need.cmaToken } : {}),
62
+ ...(need.host ? { initialHost: need.host } : {}),
63
+ summary: `Push run ${run.id} → enter credentials. Defaults reflect the run record and your saved setup.`,
64
+ });
65
+ });
66
+ const collected = await prompt({
67
+ spaceId,
68
+ environmentId,
69
+ cmaToken,
70
+ host,
71
+ });
72
+ spaceId = collected.spaceId;
73
+ environmentId = collected.environmentId;
74
+ cmaToken = collected.cmaToken;
75
+ host = collected.host;
76
+ if (!spaceId || !environmentId || !cmaToken) {
77
+ throw new Error(MISSING_CREDS_ERROR);
78
+ }
79
+ }
80
+ const result = await pushRunSession({
81
+ sessionId,
82
+ spaceId,
83
+ environmentId,
84
+ cmaToken,
85
+ ...(host ? { host } : {}),
86
+ });
87
+ if (!result.ok) {
88
+ throw new Error(result.error);
89
+ }
90
+ // Push the tokens session too when the run has one. Tokens live in their
91
+ // own pipeline.db session (separate from extract/generate), so a single
92
+ // `apply push --session <id>` only covers components. Without this second
93
+ // push, design tokens recorded by the run never reach Contentful on replay.
94
+ if (run.tokenSessionId) {
95
+ const tokenResult = await pushRunSession({
96
+ sessionId: run.tokenSessionId,
97
+ spaceId,
98
+ environmentId,
99
+ cmaToken,
100
+ ...(host ? { host } : {}),
101
+ });
102
+ if (!tokenResult.ok) {
103
+ throw new Error(tokenResult.error);
104
+ }
105
+ }
106
+ await updateRun(run.id, {
107
+ pushedTo: { spaceId, environmentId, host: host || '' },
108
+ });
109
+ const componentLine = `Pushed ${run.componentCount} component${run.componentCount === 1 ? '' : 's'} to ${spaceId}/${environmentId}`;
110
+ const tokensLine = run.tokenCount > 0 ? ` (also: ${run.tokenCount} token${run.tokenCount === 1 ? '' : 's'})` : '';
111
+ process.stdout.write(`${componentLine}${tokensLine}\n`);
112
+ }
113
+ /**
114
+ * Re-open the wizard with a prior run's extract/generate session pre-populated
115
+ * so the operator can tweak fields. Exposed as
116
+ * `experiences import --modify <id-or-path>`. Default entry step is
117
+ * `final-review`.
118
+ */
119
+ export async function modifyRun(opts) {
120
+ if (opts.saveAsNew && opts.overwrite) {
121
+ throw new Error('--save-as-new and --overwrite are mutually exclusive.');
122
+ }
123
+ const run = await resolveRunTarget(opts.runIdOrPath);
124
+ if (!opts.force) {
125
+ const staleness = await checkRunStaleness(run);
126
+ if (staleness.stale) {
127
+ throw new Error(formatStalenessRefusal(run, formatStalenessDetail(staleness)));
128
+ }
129
+ }
130
+ const saveMode = opts.overwrite ? 'overwrite' : opts.saveAsNew ? 'new' : 'prompt';
131
+ await launchModifyWizard({
132
+ extractSessionId: run.extractSessionId,
133
+ generateSessionId: run.generateSessionId,
134
+ tokenSessionId: run.tokenSessionId,
135
+ projectPath: run.projectPath,
136
+ savePath: run.savePath,
137
+ entryStep: 'final-review',
138
+ saveMode,
139
+ ...(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 } : {}),
147
+ });
148
+ }
@@ -0,0 +1,12 @@
1
+ import { type RunRecord } from './store.js';
2
+ /**
3
+ * Decide whether a positional CLI arg refers to a run-id or a filesystem path,
4
+ * then look up the corresponding RunRecord.
5
+ *
6
+ * Mirrors `git checkout` accepting either a sha or a ref: anything that *looks
7
+ * like* a path is treated as one; otherwise we fall back to a run-id lookup.
8
+ *
9
+ * Path detection: arg starts with `/`, `./`, `../`, or `~/`, OR equals `.`,
10
+ * OR equals `~`.
11
+ */
12
+ export declare function resolveRunTarget(arg: string): Promise<RunRecord>;