@embeddables/cli 0.14.4 → 0.15.0-beta.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/.prompts/custom/build-funnel.md +3 -1
- package/.prompts/custom/carousel.md +159 -0
- package/.prompts/embeddables-cli.md +116 -42
- package/.prompts/short-rule-body.md +15 -0
- package/README.md +94 -0
- package/dist/auth/index.d.ts +27 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +44 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/cli.js +69 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/build-workbench.d.ts +5 -0
- package/dist/commands/build-workbench.d.ts.map +1 -0
- package/dist/commands/build-workbench.js +117 -0
- package/dist/commands/build-workbench.js.map +1 -0
- package/dist/commands/dangerously-publish.d.ts +53 -0
- package/dist/commands/dangerously-publish.d.ts.map +1 -0
- package/dist/commands/dangerously-publish.js +731 -0
- package/dist/commands/dangerously-publish.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -253
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +41 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/provide-otp.d.ts +11 -0
- package/dist/commands/provide-otp.d.ts.map +1 -0
- package/dist/commands/provide-otp.js +102 -0
- package/dist/commands/provide-otp.js.map +1 -0
- package/dist/commands/update-project-files.d.ts +8 -0
- package/dist/commands/update-project-files.d.ts.map +1 -0
- package/dist/commands/update-project-files.js +245 -0
- package/dist/commands/update-project-files.js.map +1 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +16 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/compiler/flatten.js +1 -0
- package/dist/compiler/parsePage.js +14 -9
- package/dist/compiler/parsePage.js.map +1 -1
- package/dist/compiler/reverse.d.ts.map +1 -1
- package/dist/compiler/reverse.js +18 -8
- package/dist/compiler/reverse.js.map +1 -1
- package/dist/components/primitives/CustomHTML.d.ts +2 -1
- package/dist/components/primitives/CustomHTML.d.ts.map +1 -1
- package/dist/components/primitives/CustomHTML.js +11 -2
- package/dist/components/primitives/CustomHTML.js.map +1 -1
- package/dist/components/primitives/OptionSelector.d.ts +1 -0
- package/dist/components/primitives/OptionSelector.d.ts.map +1 -1
- package/dist/components/primitives/OptionSelector.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -3
- package/dist/constants.js.map +1 -1
- package/dist/helpers/TEMP helpers file.d.ts +1 -0
- package/dist/helpers/TEMP helpers file.d.ts.map +1 -0
- package/dist/helpers/TEMP helpers file.js +1 -0
- package/dist/helpers/json.d.ts.map +1 -1
- package/dist/helpers/json.js +132 -17
- package/dist/helpers/json.js.map +1 -1
- package/dist/prompts/branches.d.ts.map +1 -1
- package/dist/prompts/branches.js +2 -0
- package/dist/prompts/branches.js.map +1 -1
- package/dist/types-builder.d.ts +6 -2
- package/dist/types-builder.d.ts.map +1 -1
- package/dist/workbench/AutofillPanel.d.ts.map +1 -1
- package/dist/workbench/AutofillPanel.js +52 -14
- package/dist/workbench/AutofillPanel.js.map +1 -1
- package/dist/workbench/FeedbackPanel.d.ts +39 -0
- package/dist/workbench/FeedbackPanel.d.ts.map +1 -0
- package/dist/workbench/FeedbackPanel.js +279 -0
- package/dist/workbench/FeedbackPanel.js.map +1 -0
- package/dist/workbench/PageThumbnailStrip.d.ts +6 -0
- package/dist/workbench/PageThumbnailStrip.d.ts.map +1 -0
- package/dist/workbench/PageThumbnailStrip.js +124 -0
- package/dist/workbench/PageThumbnailStrip.js.map +1 -0
- package/dist/workbench/Toast.d.ts +18 -0
- package/dist/workbench/Toast.d.ts.map +1 -0
- package/dist/workbench/Toast.js +46 -0
- package/dist/workbench/Toast.js.map +1 -0
- package/dist/workbench/UserDataPanel.d.ts +2 -1
- package/dist/workbench/UserDataPanel.d.ts.map +1 -1
- package/dist/workbench/UserDataPanel.js +2 -1
- package/dist/workbench/UserDataPanel.js.map +1 -1
- package/dist/workbench/WorkbenchApp.d.ts.map +1 -1
- package/dist/workbench/WorkbenchApp.js +19 -3
- package/dist/workbench/WorkbenchApp.js.map +1 -1
- package/dist/workbench/cloudflare-worker/README.md +31 -0
- package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
- package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
- package/dist/workbench/cloudflare-worker/worker.js +40 -0
- package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
- package/dist/workbench/supabase-browser.d.ts +11 -0
- package/dist/workbench/supabase-browser.d.ts.map +1 -0
- package/dist/workbench/supabase-browser.js +18 -0
- package/dist/workbench/supabase-browser.js.map +1 -0
- package/dist/workbench/types.d.ts +25 -0
- package/dist/workbench/types.d.ts.map +1 -0
- package/dist/workbench/types.js +2 -0
- package/dist/workbench/types.js.map +1 -0
- package/dist/workbench/useComponentSelection.d.ts +27 -0
- package/dist/workbench/useComponentSelection.d.ts.map +1 -0
- package/dist/workbench/useComponentSelection.js +203 -0
- package/dist/workbench/useComponentSelection.js.map +1 -0
- package/dist/workbench/workbench.css +1614 -0
- package/dist/workbench/workbench.js +77 -0
- package/package.json +1 -1
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import * as Sentry from '@sentry/node';
|
|
5
|
+
import { getAccessToken, getAuthenticatedSupabaseClient, isLoggedIn } from '../auth/index.js';
|
|
6
|
+
import { getProjectId } from '../config/index.js';
|
|
7
|
+
import { compileAllPages } from '../compiler/index.js';
|
|
8
|
+
import { formatError } from '../compiler/errors.js';
|
|
9
|
+
import { promptForLocalEmbeddable, promptForProject } from '../prompts/index.js';
|
|
10
|
+
import { WEB_APP_BASE_URL } from '../constants.js';
|
|
11
|
+
import { captureException, createLogger, exit } from '../logger.js';
|
|
12
|
+
import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
|
|
13
|
+
import * as stdout from '../stdout.js';
|
|
14
|
+
import { translateJsonDiffToEditCommands } from '../helpers/json.js';
|
|
15
|
+
import { generateId, inferEmbeddableFromCwd, readEmbeddableContext } from '../helpers/utils.js';
|
|
16
|
+
import { withSpan } from '../tracing.js';
|
|
17
|
+
/** Slug used for `embeddable-{slug}@{version}.json` snapshots (main-line publish only). */
|
|
18
|
+
const PUBLISH_VERSION_FILE_BRANCH_SLUG = 'main';
|
|
19
|
+
class PublishError extends Error {
|
|
20
|
+
detail;
|
|
21
|
+
constructor(message, detail) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.detail = detail;
|
|
24
|
+
this.name = 'PublishError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Default network timeout for publish-related API calls. */
|
|
28
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
|
|
29
|
+
/** Server error when STAGING is requested but the version is no longer SAVED (e.g. already STAGING). */
|
|
30
|
+
const STAGING_GATE_ERROR_SUBSTRING = 'Version is not in saved status to be pushed to staging';
|
|
31
|
+
/**
|
|
32
|
+
* Runs fetch with a timeout so CLI commands do not hang indefinitely.
|
|
33
|
+
*
|
|
34
|
+
* @param url - Target request URL.
|
|
35
|
+
* @param options - Standard fetch options.
|
|
36
|
+
* @param timeoutMs - Abort timeout in milliseconds.
|
|
37
|
+
* @returns A fetch Response when the request completes in time.
|
|
38
|
+
*/
|
|
39
|
+
async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timeoutId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Normalizes abort detection across runtimes for timed-out network calls.
|
|
51
|
+
*
|
|
52
|
+
* @param error - Unknown thrown error from fetch.
|
|
53
|
+
* @returns True when error indicates an aborted request.
|
|
54
|
+
*/
|
|
55
|
+
function isAbortError(error) {
|
|
56
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
57
|
+
}
|
|
58
|
+
async function safeParseJson(response) {
|
|
59
|
+
try {
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isSaveErrorResponse(value) {
|
|
68
|
+
return (typeof value === 'object' &&
|
|
69
|
+
value !== null &&
|
|
70
|
+
'error' in value &&
|
|
71
|
+
typeof value.error === 'string');
|
|
72
|
+
}
|
|
73
|
+
function isSaveConflictResponse(value) {
|
|
74
|
+
return (typeof value === 'object' &&
|
|
75
|
+
value !== null &&
|
|
76
|
+
'latestVersionNumber' in value &&
|
|
77
|
+
'yourVersionNumber' in value &&
|
|
78
|
+
typeof value.latestVersionNumber === 'number' &&
|
|
79
|
+
typeof value.yourVersionNumber === 'number');
|
|
80
|
+
}
|
|
81
|
+
function isSaveResponse(value) {
|
|
82
|
+
return (typeof value === 'object' &&
|
|
83
|
+
value !== null &&
|
|
84
|
+
value.success === true &&
|
|
85
|
+
typeof value.data === 'object' &&
|
|
86
|
+
typeof value.data?.newVersionNumber === 'number');
|
|
87
|
+
}
|
|
88
|
+
function getVersionFromConfig(embeddableId) {
|
|
89
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
90
|
+
if (!fs.existsSync(configPath)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
95
|
+
if (typeof config._version === 'number') {
|
|
96
|
+
return config._version;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Ignore parse errors
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function setVersionInConfig(embeddableId, version) {
|
|
105
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
106
|
+
if (!fs.existsSync(configPath)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
111
|
+
config._version = version;
|
|
112
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignore errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function getLatestVersionFromFiles(generatedDir) {
|
|
119
|
+
if (!fs.existsSync(generatedDir)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const files = fs.readdirSync(generatedDir);
|
|
123
|
+
const legacyPattern = /^embeddable-v(\d+)\.json$/;
|
|
124
|
+
const branchPattern = /^embeddable-[^@]+@(\d+)\.json$/;
|
|
125
|
+
let maxVersion = null;
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const match = file.match(legacyPattern) ?? file.match(branchPattern);
|
|
128
|
+
if (match) {
|
|
129
|
+
const version = parseInt(match[1], 10);
|
|
130
|
+
if (maxVersion === null || version > maxVersion) {
|
|
131
|
+
maxVersion = version;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return maxVersion;
|
|
136
|
+
}
|
|
137
|
+
function parseCliPositiveInteger(params) {
|
|
138
|
+
const { raw } = params;
|
|
139
|
+
const isIntegerLike = /^\d+$/.test(raw);
|
|
140
|
+
const parsed = Number(raw);
|
|
141
|
+
if (!isIntegerLike || !Number.isInteger(parsed) || parsed <= 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Version to promote without save: `--publish-version`, then `config.json` `_version`,
|
|
148
|
+
* then highest version from `.generated` JSON filenames.
|
|
149
|
+
*/
|
|
150
|
+
function resolveVersionNumberForPublishOnly(params) {
|
|
151
|
+
const { publishVersionRaw, embeddableId, generatedDir } = params;
|
|
152
|
+
if (publishVersionRaw !== undefined && publishVersionRaw !== '') {
|
|
153
|
+
const parsedPublish = parseCliPositiveInteger({ raw: publishVersionRaw });
|
|
154
|
+
if (parsedPublish === null) {
|
|
155
|
+
return { ok: false, error: `Invalid --publish-version value: ${publishVersionRaw}` };
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, versionNumber: parsedPublish };
|
|
158
|
+
}
|
|
159
|
+
const configVersion = getVersionFromConfig(embeddableId);
|
|
160
|
+
if (configVersion !== null) {
|
|
161
|
+
return { ok: true, versionNumber: configVersion };
|
|
162
|
+
}
|
|
163
|
+
const latestFromFiles = getLatestVersionFromFiles(generatedDir);
|
|
164
|
+
if (latestFromFiles !== null) {
|
|
165
|
+
return { ok: true, versionNumber: latestFromFiles };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: 'Could not determine which version to publish.',
|
|
170
|
+
dim: 'Set _version in embeddables/<id>/config.json, or use --publish-version <n>. To build, save a new version, and publish it, use --save.',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function setMultipleFlowUpdates({ commands, metadata, }) {
|
|
174
|
+
const type = 'set_multiple_embeddable_updates';
|
|
175
|
+
const simpleCommandTypes = {
|
|
176
|
+
add: 'add_embeddable_property',
|
|
177
|
+
remove: 'remove_embeddable_property',
|
|
178
|
+
set: 'update_embeddable_property',
|
|
179
|
+
move: 'move_embeddable_property',
|
|
180
|
+
};
|
|
181
|
+
const mappedCommands = commands.map((command) => ({
|
|
182
|
+
type: Object.keys(simpleCommandTypes).includes(command.type)
|
|
183
|
+
? simpleCommandTypes[command.type]
|
|
184
|
+
: command.type,
|
|
185
|
+
data: command.data,
|
|
186
|
+
}));
|
|
187
|
+
return {
|
|
188
|
+
id: generateId('edit'),
|
|
189
|
+
type,
|
|
190
|
+
data: { commands: mappedCommands },
|
|
191
|
+
metadata,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Push a saved version to staging or production.
|
|
196
|
+
*/
|
|
197
|
+
async function pushToEnvironment(params) {
|
|
198
|
+
const { embeddableId, projectId, versionNumber, targetStatus, skipStagingIfAlreadyPromoted } = params;
|
|
199
|
+
const accessToken = getAccessToken();
|
|
200
|
+
if (!accessToken) {
|
|
201
|
+
throw new PublishError('Could not retrieve access token.', 'Run "embeddables login" to re-authenticate.');
|
|
202
|
+
}
|
|
203
|
+
const apiUrl = `${WEB_APP_BASE_URL}/api/embeddables/set-version-status`;
|
|
204
|
+
const headers = {
|
|
205
|
+
Authorization: `Bearer ${accessToken}`,
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
};
|
|
208
|
+
const envLabel = targetStatus === 'STAGING' ? 'staging' : 'production';
|
|
209
|
+
const requestBody = {
|
|
210
|
+
embeddableId,
|
|
211
|
+
projectId,
|
|
212
|
+
versionNumber,
|
|
213
|
+
status: targetStatus,
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
await stdout.withSpinner(`Publishing to ${envLabel} (v${versionNumber})…`, async () => {
|
|
217
|
+
const response = await fetchWithTimeout(apiUrl, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers,
|
|
220
|
+
body: JSON.stringify(requestBody),
|
|
221
|
+
});
|
|
222
|
+
if (response.ok) {
|
|
223
|
+
return { outcome: 'published' };
|
|
224
|
+
}
|
|
225
|
+
const errorData = await safeParseJson(response);
|
|
226
|
+
const errorMessage = errorData?.error ?? `HTTP ${response.status}`;
|
|
227
|
+
if (skipStagingIfAlreadyPromoted === true &&
|
|
228
|
+
targetStatus === 'STAGING' &&
|
|
229
|
+
response.status === 400 &&
|
|
230
|
+
errorMessage.includes(STAGING_GATE_ERROR_SUBSTRING)) {
|
|
231
|
+
return { outcome: 'skipped_staging_already_promoted' };
|
|
232
|
+
}
|
|
233
|
+
throw new PublishError(`Failed to publish to ${envLabel}: ${errorMessage}`);
|
|
234
|
+
}, {
|
|
235
|
+
successText: (pushResult) => pushResult.outcome === 'skipped_staging_already_promoted'
|
|
236
|
+
? 'Already on staging; continuing…'
|
|
237
|
+
: `Published to ${envLabel}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error instanceof PublishError) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
if (isAbortError(error)) {
|
|
245
|
+
throw new PublishError(`Failed to publish to ${envLabel}: request timed out after ${DEFAULT_FETCH_TIMEOUT_MS}ms`);
|
|
246
|
+
}
|
|
247
|
+
const message = error instanceof Error ? error.message : 'Network request failed';
|
|
248
|
+
throw new PublishError(`Failed to publish to ${envLabel}: ${message}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function pushToTargetEnvironments(params) {
|
|
252
|
+
const { embeddableId, projectId, versionNumber, targetEnv, viaStaging } = params;
|
|
253
|
+
if (targetEnv === 'STAGING') {
|
|
254
|
+
await pushToEnvironment({
|
|
255
|
+
embeddableId,
|
|
256
|
+
projectId,
|
|
257
|
+
versionNumber,
|
|
258
|
+
targetStatus: 'STAGING',
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (viaStaging) {
|
|
263
|
+
await pushToEnvironment({
|
|
264
|
+
embeddableId,
|
|
265
|
+
projectId,
|
|
266
|
+
versionNumber,
|
|
267
|
+
targetStatus: 'STAGING',
|
|
268
|
+
skipStagingIfAlreadyPromoted: true,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
await pushToEnvironment({
|
|
272
|
+
embeddableId,
|
|
273
|
+
projectId,
|
|
274
|
+
versionNumber,
|
|
275
|
+
targetStatus: 'PROD',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Publishes an embeddable to staging or production.
|
|
280
|
+
*
|
|
281
|
+
* By default promotes the existing server version from `config.json` `_version` (no new version).
|
|
282
|
+
* With `saveNewVersion: true`, builds, saves a new version via the API, then publishes it.
|
|
283
|
+
*/
|
|
284
|
+
export async function runDangerouslyPublish(opts) {
|
|
285
|
+
const logger = createLogger('runDangerouslyPublish');
|
|
286
|
+
const publishStart = performance.now();
|
|
287
|
+
try {
|
|
288
|
+
// Validate options
|
|
289
|
+
if (opts.staging && opts.prod) {
|
|
290
|
+
stdout.error('Cannot use --staging and --prod together.');
|
|
291
|
+
stdout.dim('Choose one target environment.');
|
|
292
|
+
await exit(1);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (!opts.staging && !opts.prod) {
|
|
296
|
+
stdout.error('Must specify --staging or --prod flag.');
|
|
297
|
+
stdout.dim('Example: embeddables dangerously-publish --staging');
|
|
298
|
+
await exit(1);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (opts.viaStaging === true && !opts.prod) {
|
|
302
|
+
stdout.error('--via-staging can only be used with --prod.');
|
|
303
|
+
stdout.dim('Example: embeddables dangerously-publish --prod --via-staging');
|
|
304
|
+
await exit(1);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (opts.saveNewVersion === true && opts.prod === true && opts.viaStaging !== true) {
|
|
308
|
+
stdout.error('--save with --prod requires --via-staging (new versions must be on staging before production).');
|
|
309
|
+
stdout.dim('Example: embeddables dangerously-publish --save --prod --via-staging');
|
|
310
|
+
await exit(1);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const targetEnv = opts.staging ? 'STAGING' : 'PROD';
|
|
314
|
+
const envLabel = opts.staging ? 'staging' : 'production';
|
|
315
|
+
// 1. Check login
|
|
316
|
+
if (!isLoggedIn()) {
|
|
317
|
+
stdout.warn('Not logged in.');
|
|
318
|
+
stdout.dim('Run "embeddables login" first.');
|
|
319
|
+
logger.error('not logged in');
|
|
320
|
+
await exit(1);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// 2. Get access token
|
|
324
|
+
const accessToken = getAccessToken();
|
|
325
|
+
if (!accessToken) {
|
|
326
|
+
stdout.warn('Could not retrieve access token.');
|
|
327
|
+
stdout.dim('Run "embeddables login" to re-authenticate.');
|
|
328
|
+
logger.error('no access token');
|
|
329
|
+
await exit(1);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// 3. Get embeddable ID
|
|
333
|
+
const inferred = inferEmbeddableFromCwd();
|
|
334
|
+
let embeddableId = opts.id ?? inferred?.embeddableId;
|
|
335
|
+
if (inferred && !opts.id && embeddableId) {
|
|
336
|
+
process.chdir(inferred.projectRoot);
|
|
337
|
+
}
|
|
338
|
+
setSentryContext(getSentryContextFromProjectConfig());
|
|
339
|
+
if (!embeddableId) {
|
|
340
|
+
const selected = await promptForLocalEmbeddable({
|
|
341
|
+
message: 'Select an embeddable to publish:',
|
|
342
|
+
});
|
|
343
|
+
if (!selected) {
|
|
344
|
+
await exit(1);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
embeddableId = selected;
|
|
348
|
+
stdout.gap();
|
|
349
|
+
}
|
|
350
|
+
if (embeddableId) {
|
|
351
|
+
const embeddableCtx = getSentryContextFromEmbeddableConfig(embeddableId);
|
|
352
|
+
setSentryContext({
|
|
353
|
+
embeddable: { id: embeddableId },
|
|
354
|
+
...embeddableCtx,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const branchGate = readEmbeddableContext(embeddableId);
|
|
358
|
+
if (branchGate.branchId) {
|
|
359
|
+
stdout.error('dangerously-publish is only for the main line. This checkout is on a branch.');
|
|
360
|
+
stdout.dim('Switch to main with embeddables branches switch, or use the Builder.');
|
|
361
|
+
logger.error('publish blocked: branch checkout', { embeddableId, branchId: branchGate.branchId });
|
|
362
|
+
await exit(1);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const branchLabel = 'main';
|
|
366
|
+
logger.info('dangerously-publish started', {
|
|
367
|
+
saveNewVersion: opts.saveNewVersion === true,
|
|
368
|
+
target: targetEnv,
|
|
369
|
+
});
|
|
370
|
+
// 4. Get project ID
|
|
371
|
+
let projectId = opts.projectId ?? getProjectId();
|
|
372
|
+
if (!projectId) {
|
|
373
|
+
stdout.step('No project configured. Fetching projects…');
|
|
374
|
+
const selectedProject = await promptForProject();
|
|
375
|
+
if (!selectedProject) {
|
|
376
|
+
await exit(1);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
projectId = selectedProject.id;
|
|
380
|
+
}
|
|
381
|
+
const generatedDir = path.join('embeddables', embeddableId, '.generated');
|
|
382
|
+
// Default: promote an existing server version (config `_version` / --publish-version / `.generated/`). No save-version.
|
|
383
|
+
if (opts.saveNewVersion !== true) {
|
|
384
|
+
const resolvedPublish = resolveVersionNumberForPublishOnly({
|
|
385
|
+
publishVersionRaw: opts.publishVersion,
|
|
386
|
+
embeddableId,
|
|
387
|
+
generatedDir,
|
|
388
|
+
});
|
|
389
|
+
if (!resolvedPublish.ok) {
|
|
390
|
+
stdout.error(resolvedPublish.error);
|
|
391
|
+
if (resolvedPublish.dim) {
|
|
392
|
+
stdout.dim(resolvedPublish.dim);
|
|
393
|
+
}
|
|
394
|
+
logger.error('could not resolve version for publish', { embeddableId });
|
|
395
|
+
await exit(1);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const versionNumberToPublish = resolvedPublish.versionNumber;
|
|
399
|
+
logger.info('dangerously-publish publish existing version', {
|
|
400
|
+
versionNumber: versionNumberToPublish,
|
|
401
|
+
target: targetEnv,
|
|
402
|
+
});
|
|
403
|
+
const publishExistingCtx = readEmbeddableContext(embeddableId);
|
|
404
|
+
stdout.embeddableInfoBox({
|
|
405
|
+
title: publishExistingCtx.title,
|
|
406
|
+
id: embeddableId,
|
|
407
|
+
branch: branchLabel,
|
|
408
|
+
version: `v${versionNumberToPublish}`,
|
|
409
|
+
});
|
|
410
|
+
setSentryContext({
|
|
411
|
+
versionNumber: versionNumberToPublish,
|
|
412
|
+
});
|
|
413
|
+
await pushToTargetEnvironments({
|
|
414
|
+
embeddableId,
|
|
415
|
+
projectId,
|
|
416
|
+
versionNumber: versionNumberToPublish,
|
|
417
|
+
targetEnv,
|
|
418
|
+
viaStaging: opts.viaStaging === true,
|
|
419
|
+
});
|
|
420
|
+
Sentry.metrics.count('cli.publish.completed', 1, {
|
|
421
|
+
attributes: {
|
|
422
|
+
embeddableId: embeddableId,
|
|
423
|
+
version: versionNumberToPublish,
|
|
424
|
+
target: targetEnv,
|
|
425
|
+
publishExisting: true,
|
|
426
|
+
viaStaging: opts.viaStaging === true,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
stdout.gap();
|
|
430
|
+
stdout.successBox(`${pc.bold(`Version ${versionNumberToPublish}`)} is live on ${envLabel} ${stdout.symbols.rocket}\n${stdout.randomMessage('save')}`, { title: 'Published' });
|
|
431
|
+
stdout.gap();
|
|
432
|
+
logger.info('publish complete', { versionNumber: versionNumberToPublish, target: targetEnv });
|
|
433
|
+
Sentry.metrics.distribution('cli.publish.duration', performance.now() - publishStart, {
|
|
434
|
+
unit: 'millisecond',
|
|
435
|
+
attributes: { embeddableId: embeddableId, target: targetEnv },
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// 5. Build (--save: create a new saved version, then publish it)
|
|
440
|
+
const outPath = path.join(generatedDir, 'embeddable.json');
|
|
441
|
+
if (!opts.skipBuild) {
|
|
442
|
+
const pagesGlob = `embeddables/${embeddableId}/pages/**/*.page.tsx`;
|
|
443
|
+
const stylesDir = path.join('embeddables', embeddableId, 'styles');
|
|
444
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
445
|
+
try {
|
|
446
|
+
await stdout.withSpinner('Building embeddable…', async () => {
|
|
447
|
+
await withSpan('publish.build', 'cli.build', async () => {
|
|
448
|
+
await compileAllPages({
|
|
449
|
+
pagesGlob,
|
|
450
|
+
outPath,
|
|
451
|
+
pageKeyFrom: 'filename',
|
|
452
|
+
stylesDir,
|
|
453
|
+
embeddableId: embeddableId,
|
|
454
|
+
configPath,
|
|
455
|
+
});
|
|
456
|
+
}, { embeddableId: embeddableId });
|
|
457
|
+
}, {
|
|
458
|
+
successText: 'Build successful',
|
|
459
|
+
failText: 'Build failed',
|
|
460
|
+
});
|
|
461
|
+
Sentry.metrics.count('cli.build.completed', 1, {
|
|
462
|
+
attributes: { command: 'dangerously-publish', embeddableId: embeddableId },
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
Sentry.metrics.count('cli.build.failed', 1, {
|
|
467
|
+
attributes: { command: 'dangerously-publish', embeddableId: embeddableId },
|
|
468
|
+
});
|
|
469
|
+
captureException(e, 'Publish build failed');
|
|
470
|
+
process.stderr.write(formatError(e));
|
|
471
|
+
logger.error('build failed');
|
|
472
|
+
await exit(1);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// 6. Read compiled JSON
|
|
477
|
+
if (!fs.existsSync(outPath)) {
|
|
478
|
+
stdout.warn(`No compiled embeddable found at ${outPath}`);
|
|
479
|
+
stdout.dim('Run "embeddables build" or "embeddables pull" first.');
|
|
480
|
+
logger.error('compiled json not found', { outPath });
|
|
481
|
+
await exit(1);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const jsonContent = fs.readFileSync(outPath, 'utf8');
|
|
485
|
+
let embeddableJson;
|
|
486
|
+
try {
|
|
487
|
+
embeddableJson = JSON.parse(jsonContent);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
stdout.error('Failed to parse embeddable JSON.');
|
|
491
|
+
logger.error('failed to parse compiled json');
|
|
492
|
+
await exit(1);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!embeddableJson.pages ||
|
|
496
|
+
!Array.isArray(embeddableJson.pages) ||
|
|
497
|
+
embeddableJson.pages.length === 0) {
|
|
498
|
+
stdout.error('Embeddable JSON must contain a non-empty pages array.');
|
|
499
|
+
logger.error('empty pages array');
|
|
500
|
+
await exit(1);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
// 7. Determine fromVersionNumber (config `_version` or highest from `.generated/` snapshots)
|
|
504
|
+
const detectedVersion = getVersionFromConfig(embeddableId) ?? getLatestVersionFromFiles(generatedDir);
|
|
505
|
+
if (detectedVersion === null) {
|
|
506
|
+
stdout.error('Could not determine the current version number.');
|
|
507
|
+
stdout.dim('Make sure you have pulled the embeddable first (embeddables pull), or that config.json has _version or .generated/ has versioned snapshots.');
|
|
508
|
+
logger.error('could not determine version');
|
|
509
|
+
await exit(1);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const fromVersionNumber = detectedVersion;
|
|
513
|
+
setSentryContext({
|
|
514
|
+
versionNumber: fromVersionNumber,
|
|
515
|
+
});
|
|
516
|
+
const ctx = readEmbeddableContext(embeddableId);
|
|
517
|
+
stdout.embeddableInfoBox({
|
|
518
|
+
title: ctx.title,
|
|
519
|
+
id: embeddableId,
|
|
520
|
+
branch: branchLabel,
|
|
521
|
+
version: `v${fromVersionNumber}`,
|
|
522
|
+
});
|
|
523
|
+
// 8. Get current user ID from the authenticated auth client.
|
|
524
|
+
const supabase = await getAuthenticatedSupabaseClient();
|
|
525
|
+
if (!supabase) {
|
|
526
|
+
throw new PublishError('Could not initialize authenticated client.', 'Run "embeddables login" to re-authenticate.');
|
|
527
|
+
}
|
|
528
|
+
const { data: { user }, error: userError, } = await supabase.auth.getUser();
|
|
529
|
+
if (userError) {
|
|
530
|
+
throw new PublishError('Could not retrieve authenticated user.', userError.message);
|
|
531
|
+
}
|
|
532
|
+
const currentUserId = user?.id;
|
|
533
|
+
if (!currentUserId) {
|
|
534
|
+
throw new PublishError('You must be logged in to publish.', 'Run "embeddables login" to authenticate.');
|
|
535
|
+
}
|
|
536
|
+
// 9. Prepare edit history
|
|
537
|
+
const previousVersionPath = path.join(generatedDir, `embeddable-${PUBLISH_VERSION_FILE_BRANCH_SLUG}@${fromVersionNumber}.json`);
|
|
538
|
+
const legacyVersionPath = path.join(generatedDir, `embeddable-v${fromVersionNumber}.json`);
|
|
539
|
+
const previousJsonContent = fs.existsSync(previousVersionPath)
|
|
540
|
+
? fs.readFileSync(previousVersionPath, 'utf8')
|
|
541
|
+
: fs.existsSync(legacyVersionPath)
|
|
542
|
+
? fs.readFileSync(legacyVersionPath, 'utf8')
|
|
543
|
+
: '{}';
|
|
544
|
+
const commands = translateJsonDiffToEditCommands({
|
|
545
|
+
previousObject: JSON.parse(previousJsonContent),
|
|
546
|
+
currentObject: JSON.parse(jsonContent),
|
|
547
|
+
basePath: [],
|
|
548
|
+
});
|
|
549
|
+
const body = {
|
|
550
|
+
userId: currentUserId,
|
|
551
|
+
embeddableId,
|
|
552
|
+
jsonString: JSON.stringify(embeddableJson),
|
|
553
|
+
projectId,
|
|
554
|
+
fromVersionNumber,
|
|
555
|
+
editHistoryLength: 1,
|
|
556
|
+
editHistoryDescriptions: [
|
|
557
|
+
{ origin: 'CLI', description: `Saved and published to ${envLabel} from CLI` },
|
|
558
|
+
],
|
|
559
|
+
editHistory: [
|
|
560
|
+
setMultipleFlowUpdates({
|
|
561
|
+
commands,
|
|
562
|
+
metadata: {
|
|
563
|
+
description: `Saved and published to ${envLabel} from CLI`,
|
|
564
|
+
trigger: { origin: 'CLI', editor: 'CLI' },
|
|
565
|
+
},
|
|
566
|
+
}),
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
if (opts.label) {
|
|
570
|
+
body.label = opts.label;
|
|
571
|
+
}
|
|
572
|
+
body.branchId = null;
|
|
573
|
+
// 10. Save to server
|
|
574
|
+
const apiUrl = `${WEB_APP_BASE_URL}/api/embeddables/save-version`;
|
|
575
|
+
const headers = {
|
|
576
|
+
Authorization: `Bearer ${accessToken}`,
|
|
577
|
+
'Content-Type': 'application/json',
|
|
578
|
+
};
|
|
579
|
+
let response;
|
|
580
|
+
const uploadStart = performance.now();
|
|
581
|
+
try {
|
|
582
|
+
response = await stdout.withSpinner(`Saving embeddable (v${fromVersionNumber})…`, async () => {
|
|
583
|
+
return withSpan('publish.upload', 'cli.save', async () => {
|
|
584
|
+
return fetchWithTimeout(apiUrl, {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers,
|
|
587
|
+
body: JSON.stringify(body),
|
|
588
|
+
});
|
|
589
|
+
}, { embeddableId: embeddableId, 'api.endpoint': apiUrl });
|
|
590
|
+
}, { successText: 'Saved to server' });
|
|
591
|
+
Sentry.metrics.distribution('cli.publish.upload_duration', performance.now() - uploadStart, {
|
|
592
|
+
unit: 'millisecond',
|
|
593
|
+
attributes: { embeddableId: embeddableId },
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch (networkError) {
|
|
597
|
+
if (isAbortError(networkError)) {
|
|
598
|
+
throw new PublishError(`Could not reach the server: request timed out after ${DEFAULT_FETCH_TIMEOUT_MS}ms`);
|
|
599
|
+
}
|
|
600
|
+
const message = networkError instanceof Error ? networkError.message : 'Network request failed';
|
|
601
|
+
throw new PublishError(`Could not reach the server: ${message}`);
|
|
602
|
+
}
|
|
603
|
+
if (response.status === 404) {
|
|
604
|
+
throw new PublishError('Save endpoint not found.');
|
|
605
|
+
}
|
|
606
|
+
if (response.status === 401 || response.status === 403) {
|
|
607
|
+
throw new PublishError('Not authorized.', 'Run "embeddables login" to re-authenticate.');
|
|
608
|
+
}
|
|
609
|
+
if (response.status >= 500) {
|
|
610
|
+
throw new PublishError(`Server error (HTTP ${response.status}). Please try again later.`);
|
|
611
|
+
}
|
|
612
|
+
if (response.status === 409) {
|
|
613
|
+
Sentry.metrics.count('cli.publish.version_conflict', 1, {
|
|
614
|
+
attributes: { embeddableId: embeddableId },
|
|
615
|
+
});
|
|
616
|
+
const conflictResult = await safeParseJson(response);
|
|
617
|
+
if (!conflictResult || !isSaveConflictResponse(conflictResult)) {
|
|
618
|
+
throw new PublishError(`Version conflict but invalid response (HTTP ${response.status}).`);
|
|
619
|
+
}
|
|
620
|
+
stdout.gap();
|
|
621
|
+
stdout.warn(`Version conflict: the server has version ${conflictResult.latestVersionNumber}, but you are saving from version ${conflictResult.yourVersionNumber}.`);
|
|
622
|
+
if (!opts.force) {
|
|
623
|
+
const { prompt: promptWithCancel } = await import('../helpers/prompt.js');
|
|
624
|
+
const { forceSave } = await promptWithCancel({
|
|
625
|
+
type: 'confirm',
|
|
626
|
+
name: 'forceSave',
|
|
627
|
+
message: 'A newer version exists on the server. Save anyway?',
|
|
628
|
+
initial: false,
|
|
629
|
+
}, 1);
|
|
630
|
+
if (!forceSave) {
|
|
631
|
+
stdout.dim('Publish cancelled.');
|
|
632
|
+
await exit(0);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Retry with force flag
|
|
637
|
+
let forceResponse;
|
|
638
|
+
try {
|
|
639
|
+
forceResponse = await stdout.withSpinner('Force-saving…', async () => {
|
|
640
|
+
return fetchWithTimeout(apiUrl, {
|
|
641
|
+
method: 'POST',
|
|
642
|
+
headers,
|
|
643
|
+
body: JSON.stringify({ ...body, force: true }),
|
|
644
|
+
});
|
|
645
|
+
}, {
|
|
646
|
+
successText: 'Force save uploaded',
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
catch (forceNetworkError) {
|
|
650
|
+
if (isAbortError(forceNetworkError)) {
|
|
651
|
+
throw new PublishError(`Retry failed: request timed out after ${DEFAULT_FETCH_TIMEOUT_MS}ms`);
|
|
652
|
+
}
|
|
653
|
+
const message = forceNetworkError instanceof Error ? forceNetworkError.message : 'Network request failed';
|
|
654
|
+
throw new PublishError(`Retry failed: ${message}`);
|
|
655
|
+
}
|
|
656
|
+
if (!forceResponse.ok) {
|
|
657
|
+
const forceResult = await safeParseJson(forceResponse);
|
|
658
|
+
const errorMessage = forceResult && isSaveErrorResponse(forceResult)
|
|
659
|
+
? forceResult.error
|
|
660
|
+
: `HTTP ${forceResponse.status}`;
|
|
661
|
+
throw new PublishError(errorMessage);
|
|
662
|
+
}
|
|
663
|
+
const forceResult = await safeParseJson(forceResponse);
|
|
664
|
+
if (!forceResult || !isSaveResponse(forceResult)) {
|
|
665
|
+
throw new PublishError('Invalid response from server after force save.');
|
|
666
|
+
}
|
|
667
|
+
response = forceResponse;
|
|
668
|
+
}
|
|
669
|
+
const result = await safeParseJson(response);
|
|
670
|
+
if (!response.ok) {
|
|
671
|
+
const errorMessage = result && isSaveErrorResponse(result) ? result.error : `HTTP ${response.status}`;
|
|
672
|
+
throw new PublishError(errorMessage);
|
|
673
|
+
}
|
|
674
|
+
if (!result || !isSaveResponse(result)) {
|
|
675
|
+
throw new PublishError('Invalid response from server.');
|
|
676
|
+
}
|
|
677
|
+
const { newVersionNumber } = result.data;
|
|
678
|
+
// Update local version
|
|
679
|
+
setVersionInConfig(embeddableId, newVersionNumber);
|
|
680
|
+
const versionedPath = path.join(generatedDir, `embeddable-${PUBLISH_VERSION_FILE_BRANCH_SLUG}@${newVersionNumber}.json`);
|
|
681
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
682
|
+
fs.writeFileSync(versionedPath, jsonContent, 'utf8');
|
|
683
|
+
// 11–12. Publish to staging and/or production (--prod alone goes straight to prod unless --via-staging)
|
|
684
|
+
await pushToTargetEnvironments({
|
|
685
|
+
embeddableId,
|
|
686
|
+
projectId,
|
|
687
|
+
versionNumber: newVersionNumber,
|
|
688
|
+
targetEnv,
|
|
689
|
+
viaStaging: opts.viaStaging === true,
|
|
690
|
+
});
|
|
691
|
+
Sentry.metrics.count('cli.publish.completed', 1, {
|
|
692
|
+
attributes: {
|
|
693
|
+
embeddableId: embeddableId,
|
|
694
|
+
version: newVersionNumber,
|
|
695
|
+
target: targetEnv,
|
|
696
|
+
viaStaging: opts.viaStaging === true,
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
stdout.gap();
|
|
700
|
+
stdout.successBox(`${pc.bold(`Version ${newVersionNumber}`)} is live on ${envLabel} ${stdout.symbols.rocket}\n${stdout.randomMessage('save')}`, { title: 'Published' });
|
|
701
|
+
stdout.gap();
|
|
702
|
+
logger.info('publish complete', { newVersionNumber, target: targetEnv });
|
|
703
|
+
Sentry.metrics.distribution('cli.publish.duration', performance.now() - publishStart, {
|
|
704
|
+
unit: 'millisecond',
|
|
705
|
+
attributes: { embeddableId: embeddableId, target: targetEnv },
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
Sentry.metrics.count('cli.publish.failed', 1, {
|
|
710
|
+
attributes: { embeddableId: opts.id ?? 'unknown' },
|
|
711
|
+
});
|
|
712
|
+
captureException(error, 'Publish failed');
|
|
713
|
+
if (error instanceof PublishError) {
|
|
714
|
+
stdout.error(`Publish failed: ${error.message}`);
|
|
715
|
+
if (error.detail) {
|
|
716
|
+
stdout.dim(error.detail);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (error instanceof Error) {
|
|
720
|
+
stdout.error(`Publish failed: ${error.message}`);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
stdout.error('Publish failed with an unexpected error.');
|
|
724
|
+
}
|
|
725
|
+
logger.error('publish failed', {
|
|
726
|
+
message: error instanceof Error ? error.message : 'unexpected error',
|
|
727
|
+
});
|
|
728
|
+
await exit(1);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
//# sourceMappingURL=dangerously-publish.js.map
|