@embeddables/cli 0.7.7 → 0.7.9

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/cli.js CHANGED
@@ -13,7 +13,9 @@ import pc from 'picocolors';
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const require = createRequire(import.meta.url);
15
15
  const pkg = require(path.join(__dirname, '..', 'package.json'));
16
+ Sentry.getCurrentScope().setAttributes({ cliVersion: pkg.version });
16
17
  import { isLoggedIn } from './auth/index.js';
18
+ import { getSentryContextFromProjectConfig, setSentryContext, setSentryUserFromAuth, } from './sentry-context.js';
17
19
  import { runBranch } from './commands/branch.js';
18
20
  import { runBuild } from './commands/build.js';
19
21
  import { runDev } from './commands/dev.js';
@@ -38,6 +40,11 @@ async function requireLogin(commandName) {
38
40
  stdout.print(pc.gray('Run "embeddables login" first.'));
39
41
  await exit(1);
40
42
  }
43
+ /** Set Sentry context from config and auth for the current command. */
44
+ async function setSentryContextForCommand() {
45
+ setSentryContext(getSentryContextFromProjectConfig());
46
+ await setSentryUserFromAuth();
47
+ }
41
48
  const program = new Command();
42
49
  program
43
50
  .name('embeddables')
@@ -50,6 +57,7 @@ program
50
57
  .option('-y, --yes', 'Skip prompts and use defaults')
51
58
  .action(async (opts) => {
52
59
  await requireLogin('init');
60
+ await setSentryContextForCommand();
53
61
  await runInit({ projectId: opts.projectId, yes: opts.yes });
54
62
  });
55
63
  program
@@ -61,6 +69,7 @@ program
61
69
  .option('--pageKeyFrom <mode>', 'filename|export', 'filename')
62
70
  .action(async (opts) => {
63
71
  await requireLogin('build');
72
+ await setSentryContextForCommand();
64
73
  await runBuild(opts);
65
74
  });
66
75
  program
@@ -76,6 +85,7 @@ program
76
85
  .option('--pageKeyFrom <mode>', 'filename|export', 'filename')
77
86
  .action(async (opts) => {
78
87
  await requireLogin('dev');
88
+ await setSentryContextForCommand();
79
89
  // --local flag overrides --engine to use local engine
80
90
  if (opts.local) {
81
91
  opts.engine = 'http://localhost:8787';
@@ -99,12 +109,17 @@ program
99
109
  .description('Pull an embeddable from the cloud')
100
110
  .option('-i, --id <id>', 'Embeddable ID to pull (interactive selection if not provided)')
101
111
  .option('-o, --out <path>', 'Output json path')
112
+ .option('--version <version>', 'Version to pull (number or "latest"); if omitted, choose from 100 most recent')
102
113
  .option('-b, --branch <branch_id>', 'Embeddable branch ID')
103
114
  .option('-f, --fix', 'Fix by removing components missing required props (warn instead of error)')
104
115
  .option('-p, --preserve', 'Preserve component order in config for forward compile')
105
116
  .action(async (opts) => {
106
117
  await requireLogin('pull');
107
- await runPull(opts);
118
+ await setSentryContextForCommand();
119
+ await runPull({
120
+ ...opts,
121
+ version: opts.version,
122
+ });
108
123
  });
109
124
  program
110
125
  .command('save')
@@ -116,6 +131,7 @@ program
116
131
  .option('--from-version <number>', 'Base version number (auto-detected from local files if not provided)')
117
132
  .action(async (opts) => {
118
133
  await requireLogin('save');
134
+ await setSentryContextForCommand();
119
135
  await runSave({
120
136
  id: opts.id,
121
137
  label: opts.label,
@@ -136,6 +152,7 @@ program
136
152
  .option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
137
153
  .action(async (opts) => {
138
154
  await requireLogin('branch');
155
+ await setSentryContextForCommand();
139
156
  await runBranch(opts);
140
157
  });
141
158
  const experiments = program.command('experiments').description('Manage embeddable experiments');
@@ -147,6 +164,7 @@ experiments
147
164
  .option('--experiment-key <key>', 'Experiment key (required if --experiment-id is set)')
148
165
  .action(async (opts) => {
149
166
  await requireLogin('experiments connect');
167
+ await setSentryContextForCommand();
150
168
  await runExperimentsConnect({
151
169
  id: opts.id,
152
170
  experimentId: opts.experimentId,
@@ -1 +1 @@
1
- {"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/commands/branch.ts"],"names":[],"mappings":"AAQA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,iBA4DpD"}
1
+ {"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/commands/branch.ts"],"names":[],"mappings":"AAYA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,iBAoEpD"}
@@ -3,6 +3,7 @@ import { isLoggedIn } from '../auth/index.js';
3
3
  import { runPull } from './pull.js';
4
4
  import { promptForLocalEmbeddable, fetchBranches, promptForBranch } from '../prompts/index.js';
5
5
  import { createLogger, exit } from '../logger.js';
6
+ import { getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
6
7
  import * as stdout from '../stdout.js';
7
8
  import { inferEmbeddableFromCwd } from '../helpers/utils.js';
8
9
  export async function runBranch(opts) {
@@ -20,6 +21,7 @@ export async function runBranch(opts) {
20
21
  if (inferred && !opts.id && embeddableId) {
21
22
  process.chdir(inferred.projectRoot);
22
23
  }
24
+ setSentryContext(getSentryContextFromProjectConfig());
23
25
  if (!embeddableId) {
24
26
  const selected = await promptForLocalEmbeddable();
25
27
  if (!selected) {
@@ -28,6 +30,9 @@ export async function runBranch(opts) {
28
30
  }
29
31
  embeddableId = selected;
30
32
  }
33
+ if (embeddableId) {
34
+ setSentryContext({ embeddable: { id: embeddableId } });
35
+ }
31
36
  stdout.print('');
32
37
  stdout.print(pc.cyan('Fetching branches...'));
33
38
  // Fetch branches for this embeddable
@@ -49,6 +54,9 @@ export async function runBranch(opts) {
49
54
  await runPull({ id: embeddableId, useMain: true });
50
55
  }
51
56
  else {
57
+ setSentryContext({
58
+ branch: { id: selectedBranch.id, name: selectedBranch.name },
59
+ });
52
60
  stdout.print(pc.cyan(`Switching to branch: ${selectedBranch.name}...`));
53
61
  stdout.print('');
54
62
  await runPull({
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAQA,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBA4CA"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAaA,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBAoDA"}
@@ -3,6 +3,7 @@ import { compileAllPages } from '../compiler/index.js';
3
3
  import { formatError } from '../compiler/errors.js';
4
4
  import { promptForLocalEmbeddable } from '../prompts/index.js';
5
5
  import { captureException, createLogger, exit } from '../logger.js';
6
+ import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
6
7
  import * as stdout from '../stdout.js';
7
8
  import { inferEmbeddableFromCwd } from '../helpers/utils.js';
8
9
  export async function runBuild(opts) {
@@ -12,6 +13,7 @@ export async function runBuild(opts) {
12
13
  if (inferred && !opts.id && embeddableId) {
13
14
  process.chdir(inferred.projectRoot);
14
15
  }
16
+ setSentryContext(getSentryContextFromProjectConfig());
15
17
  if (!embeddableId) {
16
18
  const selected = await promptForLocalEmbeddable({
17
19
  message: 'Select an embeddable to build:',
@@ -23,7 +25,13 @@ export async function runBuild(opts) {
23
25
  }
24
26
  embeddableId = selected;
25
27
  }
26
- logger.info('build started', { embeddableId });
28
+ if (embeddableId) {
29
+ setSentryContext({
30
+ embeddable: { id: embeddableId },
31
+ ...getSentryContextFromEmbeddableConfig(embeddableId),
32
+ });
33
+ }
34
+ logger.info('build started');
27
35
  const pagesGlob = opts.pages || `embeddables/${embeddableId}/pages/**/*.page.tsx`;
28
36
  const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
29
37
  const stylesDir = path.join('embeddables', embeddableId, 'styles');
@@ -40,8 +48,8 @@ export async function runBuild(opts) {
40
48
  catch (e) {
41
49
  captureException(e);
42
50
  stdout.error(formatError(e));
43
- logger.error('build failed', { embeddableId });
51
+ logger.error('build failed');
44
52
  await exit(1);
45
53
  }
46
- logger.info('build complete', { embeddableId });
54
+ logger.info('build complete');
47
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAsHA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBAiIA"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AA2HA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBAyIA"}
@@ -8,6 +8,7 @@ import { compileAllPages } from '../compiler/index.js';
8
8
  import { startProxyServer } from '../proxy/server.js';
9
9
  import { formatError } from '../compiler/errors.js';
10
10
  import { captureException, createLogger, exit } from '../logger.js';
11
+ import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
11
12
  import * as stdout from '../stdout.js';
12
13
  import { inferEmbeddableFromCwd } from '../helpers/utils.js';
13
14
  /**
@@ -101,6 +102,7 @@ export async function runDev(opts) {
101
102
  if (inferred && !opts.id && embeddableId) {
102
103
  process.chdir(inferred.projectRoot);
103
104
  }
105
+ setSentryContext(getSentryContextFromProjectConfig());
104
106
  if (!embeddableId) {
105
107
  const selected = await promptForEmbeddable();
106
108
  if (!selected) {
@@ -110,7 +112,13 @@ export async function runDev(opts) {
110
112
  }
111
113
  embeddableId = selected;
112
114
  }
113
- logger.info('dev started', { embeddableId, engine: opts.engine, port: opts.port });
115
+ if (embeddableId) {
116
+ setSentryContext({
117
+ embeddable: { id: embeddableId },
118
+ ...getSentryContextFromEmbeddableConfig(embeddableId),
119
+ });
120
+ }
121
+ logger.info('dev started', { engine: opts.engine, port: opts.port });
114
122
  const pagesGlob = opts.pages || `embeddables/${embeddableId}/pages/**/*.page.tsx`;
115
123
  const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
116
124
  const stylesDir = path.join('embeddables', embeddableId, 'styles');
@@ -135,7 +143,7 @@ export async function runDev(opts) {
135
143
  catch (e) {
136
144
  captureException(e);
137
145
  stdout.error(formatError(e));
138
- logger.error('initial build failed', { embeddableId });
146
+ logger.error('initial build failed');
139
147
  await exit(1);
140
148
  }
141
149
  // Start proxy — find an available port if the requested one is taken
@@ -163,7 +171,7 @@ export async function runDev(opts) {
163
171
  embeddableId,
164
172
  watchWorkbench: true,
165
173
  });
166
- logger.info('dev server running', { embeddableId, port });
174
+ logger.info('dev server running', { port });
167
175
  // Watch all source files: pages, styles, config, global components, computed fields, and actions
168
176
  const watcher = chokidar.watch([pagesGlob, stylesGlob, configPath, globalComponentsGlob, computedFieldsGlob, actionsGlob], {
169
177
  ignoreInitial: true,
@@ -1 +1 @@
1
- {"version":3,"file":"experiments-connect.d.ts","sourceRoot":"","sources":["../../src/commands/experiments-connect.ts"],"names":[],"mappings":"AAeA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,iBAeA"}
1
+ {"version":3,"file":"experiments-connect.d.ts","sourceRoot":"","sources":["../../src/commands/experiments-connect.ts"],"names":[],"mappings":"AAoBA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,iBAeA"}
@@ -5,6 +5,7 @@ import { isLoggedIn } from '../auth/index.js';
5
5
  import { getProjectId, writeProjectConfig } from '../config/index.js';
6
6
  import { promptForProject, promptForLocalEmbeddable, promptForExperiment, } from '../prompts/index.js';
7
7
  import { createLogger, exit } from '../logger.js';
8
+ import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
8
9
  import * as stdout from '../stdout.js';
9
10
  import { inferEmbeddableFromCwd } from '../helpers/utils.js';
10
11
  export async function runExperimentsConnect(opts) {
@@ -59,6 +60,7 @@ async function runExperimentsConnectInner(opts) {
59
60
  if (inferred && !opts.id && embeddableId) {
60
61
  process.chdir(inferred.projectRoot);
61
62
  }
63
+ setSentryContext(getSentryContextFromProjectConfig());
62
64
  if (!embeddableId) {
63
65
  const selected = await promptForLocalEmbeddable({
64
66
  message: 'Select an embeddable to connect the experiment to:',
@@ -70,6 +72,12 @@ async function runExperimentsConnectInner(opts) {
70
72
  embeddableId = selected;
71
73
  stdout.print('');
72
74
  }
75
+ if (embeddableId) {
76
+ setSentryContext({
77
+ embeddable: { id: embeddableId },
78
+ ...getSentryContextFromEmbeddableConfig(embeddableId),
79
+ });
80
+ }
73
81
  // 5. Get experiment_id and experiment_key (from opts or interactive prompt)
74
82
  let experimentId = opts.experimentId;
75
83
  let experimentKey = opts.experimentKey;
@@ -3,6 +3,8 @@ export type RunPullOptions = {
3
3
  out?: string;
4
4
  branch?: string;
5
5
  branchName?: string;
6
+ /** Version to pull (number or "latest"). If omitted and embeddable is known, prompt from 100 most recent. */
7
+ version?: string | number;
6
8
  /** When true, pull main and clear saved branch (e.g. when user explicitly selects "main" in branch switcher). */
7
9
  useMain?: boolean;
8
10
  fix?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AAkHA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iHAAiH;IACjH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,iBA8QjD"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AA6HA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6GAA6G;IAC7G,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,iHAAiH;IACjH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,iBA+SjD"}
@@ -5,8 +5,9 @@ import prompts from 'prompts';
5
5
  import { reverseCompile } from '../compiler/reverse.js';
6
6
  import { getAccessToken, isLoggedIn } from '../auth/index.js';
7
7
  import { getProjectId, writeProjectConfig } from '../config/index.js';
8
- import { promptForProject, promptForEmbeddable, fetchEmbeddableMetadata } from '../prompts/index.js';
8
+ import { promptForProject, promptForEmbeddable, fetchEmbeddableMetadata, fetchRecentVersions, promptForVersion, } from '../prompts/index.js';
9
9
  import { captureException, createLogger, exit } from '../logger.js';
10
+ import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
10
11
  import * as stdout from '../stdout.js';
11
12
  import { inferEmbeddableFromCwd } from '../helpers/utils.js';
12
13
  /**
@@ -117,6 +118,8 @@ export async function runPull(opts) {
117
118
  if (inferred && !opts.id && embeddableId) {
118
119
  process.chdir(inferred.projectRoot);
119
120
  }
121
+ // Re-apply project/org from config after chdir so embeddables.json in project root is used
122
+ setSentryContext(getSentryContextFromProjectConfig());
120
123
  // If no ID provided, try to get it interactively
121
124
  if (!embeddableId) {
122
125
  if (!isLoggedIn()) {
@@ -162,21 +165,44 @@ export async function runPull(opts) {
162
165
  embeddableId = selected;
163
166
  stdout.print('');
164
167
  }
168
+ if (embeddableId) {
169
+ setSentryContext({
170
+ embeddable: { id: embeddableId },
171
+ ...getSentryContextFromEmbeddableConfig(embeddableId),
172
+ });
173
+ }
165
174
  // When useMain, pull main and ignore/clear saved branch. Otherwise stay on current branch when no --branch.
166
175
  const currentFromConfig = opts.useMain || opts.branch != null ? null : getCurrentBranchFromConfig(embeddableId);
167
176
  const effectiveBranch = opts.useMain ? undefined : (opts.branch ?? currentFromConfig?.branchId);
168
177
  const effectiveBranchName = opts.useMain ? undefined : (opts.branchName ?? currentFromConfig?.branchName);
169
- let url = `https://engine.embeddables.com/${embeddableId}?version=latest`;
170
- if (effectiveBranch) {
171
- url += `&embeddable_branch=${effectiveBranch}`;
172
- }
173
178
  const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
174
179
  const branchLabel = effectiveBranch
175
180
  ? effectiveBranchName
176
181
  ? `${effectiveBranchName} (${effectiveBranch})`
177
182
  : effectiveBranch
178
183
  : 'main';
179
- logger.info('pull started', { embeddableId, branch: effectiveBranch ?? 'main' });
184
+ if (effectiveBranch) {
185
+ setSentryContext({ branch: { id: effectiveBranch, name: effectiveBranchName ?? null } });
186
+ }
187
+ // Resolve version: explicit --version, or interactive choice from 100 most recent
188
+ let versionParam;
189
+ if (opts.version !== undefined && opts.version !== '') {
190
+ const v = String(opts.version).toLowerCase();
191
+ versionParam = v === 'latest' ? 'latest' : v;
192
+ }
193
+ else {
194
+ stdout.print(pc.cyan('Fetching recent versions...'));
195
+ const recentVersions = await fetchRecentVersions(embeddableId, effectiveBranch ?? null);
196
+ const selected = await promptForVersion(recentVersions, {
197
+ message: 'Select a version to pull:',
198
+ });
199
+ versionParam = selected === null ? 'latest' : String(selected);
200
+ stdout.print('');
201
+ }
202
+ let url = `https://engine.embeddables.com/${embeddableId}?version=${versionParam}`;
203
+ if (effectiveBranch)
204
+ url += `&embeddable_branch=${effectiveBranch}`;
205
+ logger.info('pull started');
180
206
  stdout.print(pc.cyan(`Pulling branch: ${pc.bold(branchLabel)}`));
181
207
  stdout.print(`Fetching embeddable from ${url}...`);
182
208
  let pullVersion;
@@ -227,6 +253,11 @@ export async function runPull(opts) {
227
253
  fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf8');
228
254
  stdout.print(pc.cyan(`✓ Wrote flow metadata to ${metadataPath}`));
229
255
  }
256
+ const versionForSentry = pullVersion != null && !isNaN(Number(pullVersion)) ? Number(pullVersion) : undefined;
257
+ setSentryContext({
258
+ embeddable: { id: embeddableId, title: metadata?.title ?? null },
259
+ versionNumber: versionForSentry,
260
+ });
230
261
  // Clear existing pages, styles, computed-fields, actions, and global-components before generating new ones
231
262
  const pagesDir = path.join('embeddables', embeddableId, 'pages');
232
263
  const stylesDir = path.join('embeddables', embeddableId, 'styles');
@@ -334,7 +365,7 @@ export async function runPull(opts) {
334
365
  stdout.print(pc.cyan(`✓ Wrote versioned embeddable JSON to ${versionedPath} (with fixes applied)`));
335
366
  }
336
367
  }
337
- logger.info('pull complete', { embeddableId, version: String(pullVersion ?? 'unknown') });
368
+ logger.info('pull complete', { version: String(pullVersion ?? 'unknown') });
338
369
  }
339
370
  catch (error) {
340
371
  captureException(error);
@@ -1 +1 @@
1
- {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,cAAc,EACf,MAAM,qBAAqB,CAAA;AAkC5B,MAAM,MAAM,sBAAsB,GAC9B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAE3B,MAAM,MAAM,2BAA2B,GAAG;IACxC,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,uBAAuB,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAClD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,WAAW,CAAC,EAAE,cAAc,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAAA;AAkND,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,iBAqBA"}
1
+ {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,cAAc,EACf,MAAM,qBAAqB,CAAA;AAuC5B,MAAM,MAAM,sBAAsB,GAC9B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAE3B,MAAM,MAAM,2BAA2B,GAAG;IACxC,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,uBAAuB,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAClD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,WAAW,CAAC,EAAE,cAAc,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAAA;AAkND,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,iBAqBA"}
@@ -9,6 +9,7 @@ import { formatError } from '../compiler/errors.js';
9
9
  import { promptForLocalEmbeddable, promptForProject } from '../prompts/index.js';
10
10
  import { WEB_APP_BASE_URL } from '../constants.js';
11
11
  import { captureException, createLogger, exit } from '../logger.js';
12
+ import { getSentryContextFromEmbeddableConfig, getSentryContextFromProjectConfig, setSentryContext, } from '../sentry-context.js';
12
13
  import * as stdout from '../stdout.js';
13
14
  import { translateJsonDiffToEditCommands } from '../helpers/json.js';
14
15
  import { generateId, inferEmbeddableFromCwd } from '../helpers/utils.js';
@@ -239,6 +240,7 @@ async function runSaveInner(opts) {
239
240
  if (inferred && !opts.id && embeddableId) {
240
241
  process.chdir(inferred.projectRoot);
241
242
  }
243
+ setSentryContext(getSentryContextFromProjectConfig());
242
244
  if (!embeddableId) {
243
245
  const selected = await promptForLocalEmbeddable({
244
246
  message: 'Select an embeddable to save:',
@@ -250,9 +252,16 @@ async function runSaveInner(opts) {
250
252
  embeddableId = selected;
251
253
  stdout.print('');
252
254
  }
255
+ if (embeddableId) {
256
+ const embeddableCtx = getSentryContextFromEmbeddableConfig(embeddableId);
257
+ setSentryContext({
258
+ embeddable: { id: embeddableId },
259
+ ...embeddableCtx,
260
+ });
261
+ }
253
262
  // Resolve branch: explicit -b flag wins, otherwise use current branch from config (set by `embeddables branch`)
254
263
  const effectiveBranch = opts.branch ?? getBranchFromConfig(embeddableId) ?? undefined;
255
- logger.info('save started', { embeddableId, branch: effectiveBranch, fromVersion: opts.fromVersion });
264
+ logger.info('save started', { fromVersion: opts.fromVersion });
256
265
  // 4. Get project ID (from config or interactive prompt)
257
266
  let projectId = getProjectId();
258
267
  if (!projectId) {
@@ -293,7 +302,7 @@ async function runSaveInner(opts) {
293
302
  catch (e) {
294
303
  captureException(e);
295
304
  stdout.error(formatError(e));
296
- logger.error('build failed', { embeddableId });
305
+ logger.error('build failed');
297
306
  await exit(1);
298
307
  return;
299
308
  }
@@ -351,6 +360,18 @@ async function runSaveInner(opts) {
351
360
  }
352
361
  fromVersionNumber = detectedVersion;
353
362
  }
363
+ const embeddableCtxForBranch = getSentryContextFromEmbeddableConfig(embeddableId);
364
+ setSentryContext({
365
+ versionNumber: fromVersionNumber,
366
+ branch: effectiveBranch ?
367
+ {
368
+ id: effectiveBranch,
369
+ name: embeddableCtxForBranch.branch?.id === effectiveBranch
370
+ ? embeddableCtxForBranch.branch?.name ?? null
371
+ : null,
372
+ }
373
+ : undefined,
374
+ });
354
375
  // 7b. Check for other users' drafts on this version; warn and optionally abort
355
376
  let currentUserId = null;
356
377
  const supabase = await getAuthenticatedSupabaseClient();
@@ -517,7 +538,7 @@ async function runSaveInner(opts) {
517
538
  }
518
539
  const { newVersionNumber } = forceResult.data;
519
540
  stdout.print(pc.green(`✓ Saved as version ${newVersionNumber}`));
520
- logger.info('save complete', { embeddableId, newVersionNumber, forced: true });
541
+ logger.info('save complete', { newVersionNumber, forced: true });
521
542
  setVersionInConfig(embeddableId, newVersionNumber);
522
543
  const branchSlug = getBranchSlugFromConfig(embeddableId);
523
544
  const versionedPath = path.join(generatedDir, `embeddable-${branchSlug}@${newVersionNumber}.json`);
@@ -536,7 +557,7 @@ async function runSaveInner(opts) {
536
557
  }
537
558
  const { newVersionNumber } = result.data;
538
559
  stdout.print(pc.green(`✓ Saved as version ${newVersionNumber}`));
539
- logger.info('save complete', { embeddableId, newVersionNumber });
560
+ logger.info('save complete', { newVersionNumber });
540
561
  // Update _version in config.json so future saves know the base version
541
562
  setVersionInConfig(embeddableId, newVersionNumber);
542
563
  // Also save the versioned file to .generated/ as a snapshot (embeddable-{branch}@{version}.json)
@@ -4,6 +4,8 @@ export { fetchProjectEmbeddables, fetchEmbeddableMetadata, promptForEmbeddable,
4
4
  export type { EmbeddableInfo, EmbeddableMetadata, LocalEmbeddable, PromptForEmbeddableOptions, } from './embeddables.js';
5
5
  export { fetchBranches, promptForBranch } from './branches.js';
6
6
  export type { BranchInfo } from './branches.js';
7
+ export { fetchRecentVersions, promptForVersion } from './versions.js';
8
+ export type { VersionInfo } from './versions.js';
7
9
  export { fetchProjectExperiments, promptForExperiment, } from './experiments.js';
8
10
  export type { ExperimentInfo, PromptForExperimentOptions } from './experiments.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAC/D,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAEzE,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,0BAA0B,GAC3B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,OAAO,EACL,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,cAAc,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAC/D,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAEzE,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,0BAA0B,GAC3B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,EACL,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,cAAc,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA"}
@@ -2,4 +2,5 @@
2
2
  export { fetchProjects, promptForProject } from './projects.js';
3
3
  export { fetchProjectEmbeddables, fetchEmbeddableMetadata, promptForEmbeddable, discoverLocalEmbeddables, promptForLocalEmbeddable, } from './embeddables.js';
4
4
  export { fetchBranches, promptForBranch } from './branches.js';
5
+ export { fetchRecentVersions, promptForVersion } from './versions.js';
5
6
  export { fetchProjectExperiments, promptForExperiment, } from './experiments.js';
@@ -0,0 +1,18 @@
1
+ export interface VersionInfo {
2
+ version_number: number;
3
+ created_at: string | null;
4
+ }
5
+ /**
6
+ * Fetch up to 100 most recent version numbers for an embeddable (flow).
7
+ * Uses flow_versions for the given flow_id and branch (null = main).
8
+ * Returns unique version numbers in descending order.
9
+ */
10
+ export declare function fetchRecentVersions(flowId: string, branchId: string | null): Promise<VersionInfo[]>;
11
+ /**
12
+ * Prompt the user to select a version from the list.
13
+ * Returns the selected version number, or null for "Latest".
14
+ */
15
+ export declare function promptForVersion(versions: VersionInfo[], options?: {
16
+ message?: string;
17
+ }): Promise<number | null>;
18
+ //# sourceMappingURL=versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versions.d.ts","sourceRoot":"","sources":["../../src/prompts/versions.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAID;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,WAAW,EAAE,CAAC,CA8CxB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkDxB"}
@@ -0,0 +1,99 @@
1
+ import pc from 'picocolors';
2
+ import prompts from 'prompts';
3
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
4
+ import * as stdout from '../stdout.js';
5
+ import { formatDate } from '../helpers/dates.js';
6
+ const RECENT_VERSIONS_LIMIT = 100;
7
+ /**
8
+ * Fetch up to 100 most recent version numbers for an embeddable (flow).
9
+ * Uses flow_versions for the given flow_id and branch (null = main).
10
+ * Returns unique version numbers in descending order.
11
+ */
12
+ export async function fetchRecentVersions(flowId, branchId) {
13
+ const supabase = await getAuthenticatedSupabaseClient();
14
+ if (!supabase) {
15
+ return [];
16
+ }
17
+ try {
18
+ let query = supabase
19
+ .from('flow_versions')
20
+ .select('version_number, created_at')
21
+ .eq('flow_id', flowId)
22
+ .order('version_number', { ascending: false })
23
+ .limit(RECENT_VERSIONS_LIMIT * 2); // fetch extra then dedupe
24
+ if (branchId === null) {
25
+ query = query.is('branch_id', null);
26
+ }
27
+ else {
28
+ query = query.eq('branch_id', branchId);
29
+ }
30
+ const { data, error } = await query;
31
+ if (error) {
32
+ stdout.warn(pc.yellow(`Could not fetch versions: ${error.message}`));
33
+ return [];
34
+ }
35
+ // Dedupe by version_number, preserving order (most recent first)
36
+ const seen = new Set();
37
+ const versions = [];
38
+ for (const row of data || []) {
39
+ const v = row.version_number;
40
+ if (typeof v !== 'number' || seen.has(v))
41
+ continue;
42
+ seen.add(v);
43
+ versions.push({
44
+ version_number: v,
45
+ created_at: row.created_at ?? null,
46
+ });
47
+ if (versions.length >= RECENT_VERSIONS_LIMIT)
48
+ break;
49
+ }
50
+ return versions;
51
+ }
52
+ catch (err) {
53
+ stdout.warn(pc.yellow(`Could not fetch versions: ${err}`));
54
+ return [];
55
+ }
56
+ }
57
+ /**
58
+ * Prompt the user to select a version from the list.
59
+ * Returns the selected version number, or null for "Latest".
60
+ */
61
+ export async function promptForVersion(versions, options = {}) {
62
+ const choices = [];
63
+ if (versions.length === 0) {
64
+ choices.push({
65
+ title: pc.bold('Latest'),
66
+ value: 'latest',
67
+ description: 'Current published version',
68
+ });
69
+ }
70
+ else {
71
+ for (let i = 0; i < versions.length; i++) {
72
+ const v = versions[i];
73
+ const date = v.created_at ? formatDate(v.created_at) : '';
74
+ const isLatest = i === 0;
75
+ choices.push({
76
+ title: isLatest ? `Version ${v.version_number} (Latest)` : `Version ${v.version_number}`,
77
+ value: isLatest ? 'latest' : v.version_number,
78
+ description: date || undefined,
79
+ });
80
+ }
81
+ }
82
+ const response = await prompts({
83
+ type: 'autocomplete',
84
+ name: 'version',
85
+ message: options.message ?? 'Select a version to pull:',
86
+ choices,
87
+ suggest: (input, choicesList) => Promise.resolve(choicesList.filter((c) => c.value === 'latest' ||
88
+ String(c.value).includes(input) ||
89
+ (typeof c.title === 'string' && c.title.toLowerCase().includes(input.toLowerCase())))),
90
+ }, {
91
+ onCancel: () => {
92
+ process.exit(0);
93
+ },
94
+ });
95
+ if (response.version === 'latest' || response.version === undefined) {
96
+ return null;
97
+ }
98
+ return typeof response.version === 'number' ? response.version : null;
99
+ }
@@ -0,0 +1,48 @@
1
+ export interface SentryContextUser {
2
+ id: string;
3
+ email?: string | null;
4
+ }
5
+ export interface SentryContextProject {
6
+ id: string;
7
+ title?: string | null;
8
+ }
9
+ export interface SentryContextOrg {
10
+ id: string;
11
+ title?: string | null;
12
+ }
13
+ export interface SentryContextEmbeddable {
14
+ id: string;
15
+ title?: string | null;
16
+ }
17
+ export interface SentryContextBranch {
18
+ id: string;
19
+ name?: string | null;
20
+ }
21
+ export interface SentryContextInput {
22
+ user?: SentryContextUser | null;
23
+ project?: SentryContextProject | null;
24
+ org?: SentryContextOrg | null;
25
+ embeddable?: SentryContextEmbeddable | null;
26
+ branch?: SentryContextBranch | null;
27
+ versionNumber?: number | null;
28
+ }
29
+ /**
30
+ * Set Sentry scope with the given context. Only sets keys that are present and have valid values.
31
+ * Uses setUser/setContext for error events and setAttributes on the current scope so values
32
+ * (project, org, embeddable, branch, versionNumber) are included in log events.
33
+ */
34
+ export declare function setSentryContext(ctx: SentryContextInput): void;
35
+ /**
36
+ * Return project and org context from embeddables.json when present.
37
+ */
38
+ export declare function getSentryContextFromProjectConfig(): Partial<SentryContextInput>;
39
+ /**
40
+ * Return embeddable title (from metadata.json), branch and versionNumber (from config.json)
41
+ * for embeddables/{embeddableId}/ when present.
42
+ */
43
+ export declare function getSentryContextFromEmbeddableConfig(embeddableId: string): Partial<SentryContextInput>;
44
+ /**
45
+ * Set Sentry user from auth when logged in. Call after requireLogin for commands that need user context.
46
+ */
47
+ export declare function setSentryUserFromAuth(): Promise<void>;
48
+ //# sourceMappingURL=sentry-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry-context.d.ts","sourceRoot":"","sources":["../src/sentry-context.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC/B,OAAO,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAA;IACrC,GAAG,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC7B,UAAU,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAA;IAC3C,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACnC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI,CA4D9D;AAED;;GAEG;AACH,wBAAgB,iCAAiC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAW/E;AAED;;;GAGG;AACH,wBAAgB,oCAAoC,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA6CtG;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAY3D"}
@@ -0,0 +1,156 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import * as Sentry from '@sentry/node';
4
+ import { readProjectConfig } from './config/index.js';
5
+ /**
6
+ * Set Sentry scope with the given context. Only sets keys that are present and have valid values.
7
+ * Uses setUser/setContext for error events and setAttributes on the current scope so values
8
+ * (project, org, embeddable, branch, versionNumber) are included in log events.
9
+ */
10
+ export function setSentryContext(ctx) {
11
+ if (ctx.user?.id) {
12
+ Sentry.setUser({
13
+ id: ctx.user.id,
14
+ email: ctx.user.email ?? undefined,
15
+ });
16
+ }
17
+ if (ctx.project?.id) {
18
+ Sentry.setContext('project', {
19
+ id: ctx.project.id,
20
+ title: ctx.project.title ?? undefined,
21
+ });
22
+ }
23
+ if (ctx.org?.id) {
24
+ Sentry.setContext('org', {
25
+ id: ctx.org.id,
26
+ title: ctx.org.title ?? undefined,
27
+ });
28
+ }
29
+ if (ctx.embeddable?.id) {
30
+ Sentry.setContext('embeddable', {
31
+ id: ctx.embeddable.id,
32
+ title: ctx.embeddable.title ?? undefined,
33
+ });
34
+ }
35
+ if (ctx.branch?.id) {
36
+ Sentry.setContext('branch', {
37
+ id: ctx.branch.id,
38
+ name: ctx.branch.name ?? undefined,
39
+ });
40
+ }
41
+ if (ctx.versionNumber != null && !isNaN(ctx.versionNumber)) {
42
+ Sentry.setContext('version', { versionNumber: ctx.versionNumber });
43
+ }
44
+ // Scope attributes are included in log events; only string/number/boolean are supported
45
+ const scope = Sentry.getCurrentScope();
46
+ const attrs = {};
47
+ if (ctx.project?.id) {
48
+ attrs.projectId = ctx.project.id;
49
+ if (ctx.project.title != null)
50
+ attrs.projectTitle = String(ctx.project.title);
51
+ }
52
+ if (ctx.org?.id) {
53
+ attrs.orgId = ctx.org.id;
54
+ if (ctx.org.title != null)
55
+ attrs.orgTitle = String(ctx.org.title);
56
+ }
57
+ if (ctx.embeddable?.id) {
58
+ attrs.embeddableId = ctx.embeddable.id;
59
+ if (ctx.embeddable.title != null)
60
+ attrs.embeddableTitle = String(ctx.embeddable.title);
61
+ }
62
+ if (ctx.branch?.id) {
63
+ attrs.branchId = ctx.branch.id;
64
+ if (ctx.branch.name != null)
65
+ attrs.branchName = String(ctx.branch.name);
66
+ }
67
+ if (ctx.versionNumber != null && !isNaN(ctx.versionNumber)) {
68
+ attrs.versionNumber = ctx.versionNumber;
69
+ }
70
+ if (Object.keys(attrs).length > 0) {
71
+ scope.setAttributes(attrs);
72
+ }
73
+ }
74
+ /**
75
+ * Return project and org context from embeddables.json when present.
76
+ */
77
+ export function getSentryContextFromProjectConfig() {
78
+ const config = readProjectConfig();
79
+ if (!config)
80
+ return {};
81
+ const out = {};
82
+ if (config.project_id) {
83
+ out.project = { id: config.project_id, title: config.project_name ?? null };
84
+ }
85
+ if (config.org_id) {
86
+ out.org = { id: config.org_id, title: config.org_title ?? null };
87
+ }
88
+ return out;
89
+ }
90
+ /**
91
+ * Return embeddable title (from metadata.json), branch and versionNumber (from config.json)
92
+ * for embeddables/{embeddableId}/ when present.
93
+ */
94
+ export function getSentryContextFromEmbeddableConfig(embeddableId) {
95
+ const out = {};
96
+ const embeddableDir = path.join('embeddables', embeddableId);
97
+ // Embeddable title from metadata.json (written by pull)
98
+ const metadataPath = path.join(embeddableDir, 'metadata.json');
99
+ if (fs.existsSync(metadataPath)) {
100
+ try {
101
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
102
+ const title = metadata.title;
103
+ if (typeof title === 'string' && title) {
104
+ out.embeddable = { id: embeddableId, title };
105
+ }
106
+ }
107
+ catch {
108
+ // ignore
109
+ }
110
+ }
111
+ if (!out.embeddable) {
112
+ out.embeddable = { id: embeddableId };
113
+ }
114
+ const configPath = path.join(embeddableDir, 'config.json');
115
+ if (!fs.existsSync(configPath))
116
+ return out;
117
+ try {
118
+ const raw = fs.readFileSync(configPath, 'utf8');
119
+ const config = JSON.parse(raw);
120
+ const branchId = config._branch_id;
121
+ if (typeof branchId === 'string' && branchId) {
122
+ const branchName = config._branch_name;
123
+ out.branch = {
124
+ id: branchId,
125
+ name: typeof branchName === 'string' ? branchName : null,
126
+ };
127
+ }
128
+ else if (config._version != null) {
129
+ // Config was written by pull; no _branch_id means we're on main
130
+ out.branch = { id: 'main', name: 'main' };
131
+ }
132
+ const ver = config._version;
133
+ if (typeof ver === 'number' && !isNaN(ver)) {
134
+ out.versionNumber = ver;
135
+ }
136
+ return out;
137
+ }
138
+ catch {
139
+ return out;
140
+ }
141
+ }
142
+ /**
143
+ * Set Sentry user from auth when logged in. Call after requireLogin for commands that need user context.
144
+ */
145
+ export async function setSentryUserFromAuth() {
146
+ const { getAuthenticatedSupabaseClient } = await import('./auth/index.js');
147
+ const supabase = await getAuthenticatedSupabaseClient();
148
+ if (!supabase)
149
+ return;
150
+ const { data: { user }, } = await supabase.auth.getUser();
151
+ if (user?.id) {
152
+ setSentryContext({
153
+ user: { id: user.id, email: user.email ?? null },
154
+ });
155
+ }
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddables/cli",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "embeddables": "./bin/embeddables.mjs"