@geraldmaron/construct 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ /**
2
+ * lib/config/source-targets.mjs — typed integration source targets in construct.config.json.
3
+ *
4
+ * Validates GitHub, Jira, Linear, and Slack selector records, merges legacy env
5
+ * target lists additively, deduplicates by provider+selector signature, and
6
+ * converts resolved targets into embed provider source records for auto-discovery.
7
+ */
8
+
9
+ export const SOURCE_PROVIDERS = Object.freeze(['github', 'jira', 'linear', 'slack']);
10
+
11
+ export const SLACK_INTENTS = Object.freeze(['internal', 'risk', 'decision', 'external', 'how-to']);
12
+
13
+ const ID_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
14
+ const GITHUB_REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
15
+ const JIRA_PROJECT_RE = /^[A-Z][A-Z0-9]{1,9}$/;
16
+
17
+ export function validateSourceTarget(target, index = 0) {
18
+ const errors = [];
19
+ const at = `sources.targets[${index}]`;
20
+
21
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
22
+ return [`${at}: must be an object`];
23
+ }
24
+
25
+ if (typeof target.id !== 'string' || !ID_RE.test(target.id)) {
26
+ errors.push(`${at}.id: required stable id (letters, numbers, hyphens, underscores; max 64 chars)`);
27
+ }
28
+
29
+ if (!SOURCE_PROVIDERS.includes(target.provider)) {
30
+ errors.push(`${at}.provider: must be one of ${SOURCE_PROVIDERS.join(', ')}`);
31
+ }
32
+
33
+ if (!target.selector || typeof target.selector !== 'object' || Array.isArray(target.selector)) {
34
+ errors.push(`${at}.selector: required object`);
35
+ return errors;
36
+ }
37
+
38
+ const sel = target.selector;
39
+ switch (target.provider) {
40
+ case 'github':
41
+ if (typeof sel.repo !== 'string' || !GITHUB_REPO_RE.test(sel.repo.trim())) {
42
+ errors.push(`${at}.selector.repo: required owner/repo slug`);
43
+ }
44
+ break;
45
+ case 'jira':
46
+ if (typeof sel.project !== 'string' || !JIRA_PROJECT_RE.test(sel.project.trim().toUpperCase())) {
47
+ errors.push(`${at}.selector.project: required Jira project key (2–10 uppercase letters/digits)`);
48
+ }
49
+ break;
50
+ case 'linear':
51
+ if (typeof sel.team !== 'string' || !sel.team.trim()) {
52
+ errors.push(`${at}.selector.team: required Linear team key or name`);
53
+ }
54
+ break;
55
+ case 'slack': {
56
+ if (typeof sel.channel !== 'string' || !sel.channel.trim()) {
57
+ errors.push(`${at}.selector.channel: required Slack channel name or ID`);
58
+ }
59
+ if (sel.intent !== undefined && !SLACK_INTENTS.includes(sel.intent)) {
60
+ errors.push(`${at}.selector.intent: must be one of ${SLACK_INTENTS.join(', ')}`);
61
+ }
62
+ break;
63
+ }
64
+ default:
65
+ break;
66
+ }
67
+
68
+ return errors;
69
+ }
70
+
71
+ export function validateSourceTargets(targets) {
72
+ const errors = [];
73
+ if (targets === undefined) return errors;
74
+ if (!Array.isArray(targets)) {
75
+ return ['sources.targets: must be an array'];
76
+ }
77
+
78
+ const seenIds = new Set();
79
+ for (let i = 0; i < targets.length; i++) {
80
+ errors.push(...validateSourceTarget(targets[i], i));
81
+ const id = targets[i]?.id;
82
+ if (typeof id === 'string') {
83
+ const key = id.toLowerCase();
84
+ if (seenIds.has(key)) {
85
+ errors.push(`sources.targets[${i}].id: duplicate id "${id}"`);
86
+ }
87
+ seenIds.add(key);
88
+ }
89
+ }
90
+ return errors;
91
+ }
92
+
93
+ function parseCsv(value) {
94
+ return String(value ?? '')
95
+ .split(',')
96
+ .map((s) => s.trim())
97
+ .filter(Boolean);
98
+ }
99
+
100
+ function slugId(value) {
101
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
102
+ }
103
+
104
+ function parseSlackChannelEntry(raw) {
105
+ const trimmed = String(raw).trim().replace(/^#/, '');
106
+ const colon = trimmed.indexOf(':');
107
+ if (colon === -1) {
108
+ return { channel: trimmed, intent: 'internal' };
109
+ }
110
+ return { channel: trimmed.slice(0, colon), intent: trimmed.slice(colon + 1) || 'internal' };
111
+ }
112
+
113
+ export function legacyEnvSourceTargets(env = process.env) {
114
+ const targets = [];
115
+
116
+ for (const repo of parseCsv(env.GITHUB_REPOS ?? env.GITHUB_REPO)) {
117
+ targets.push({
118
+ id: `env-github-${slugId(repo)}`,
119
+ provider: 'github',
120
+ selector: { repo },
121
+ provenance: 'env:GITHUB_REPOS',
122
+ });
123
+ }
124
+
125
+ for (const project of parseCsv(env.JIRA_PROJECTS)) {
126
+ const key = project.toUpperCase();
127
+ targets.push({
128
+ id: `env-jira-${slugId(key)}`,
129
+ provider: 'jira',
130
+ selector: { project: key },
131
+ provenance: 'env:JIRA_PROJECTS',
132
+ });
133
+ }
134
+
135
+ for (const team of parseCsv(env.LINEAR_TEAMS)) {
136
+ targets.push({
137
+ id: `env-linear-${slugId(team)}`,
138
+ provider: 'linear',
139
+ selector: { team },
140
+ provenance: 'env:LINEAR_TEAMS',
141
+ });
142
+ }
143
+
144
+ for (const entry of parseCsv(env.SLACK_CHANNELS ?? env.SLACK_CHANNEL)) {
145
+ const { channel, intent } = parseSlackChannelEntry(entry);
146
+ targets.push({
147
+ id: `env-slack-${slugId(channel)}`,
148
+ provider: 'slack',
149
+ selector: { channel, intent: SLACK_INTENTS.includes(intent) ? intent : 'internal' },
150
+ provenance: 'env:SLACK_CHANNELS',
151
+ });
152
+ }
153
+
154
+ return targets;
155
+ }
156
+
157
+ export function targetSignature(target) {
158
+ const sel = target.selector ?? {};
159
+ switch (target.provider) {
160
+ case 'github':
161
+ return `github:repo:${sel.repo?.trim().toLowerCase()}`;
162
+ case 'jira':
163
+ return `jira:project:${String(sel.project ?? '').trim().toUpperCase()}`;
164
+ case 'linear':
165
+ return `linear:team:${String(sel.team ?? '').trim().toLowerCase()}`;
166
+ case 'slack':
167
+ return `slack:channel:${String(sel.channel ?? '').trim().toLowerCase()}:intent:${sel.intent ?? 'internal'}`;
168
+ default:
169
+ return `${target.provider}:${JSON.stringify(sel)}`;
170
+ }
171
+ }
172
+
173
+ export function mergeSourceTargets(configTargets = [], envTargets = []) {
174
+ const merged = [];
175
+ const seen = new Set();
176
+
177
+ for (const target of [...configTargets, ...envTargets]) {
178
+ const sig = targetSignature(target);
179
+ if (seen.has(sig)) continue;
180
+ seen.add(sig);
181
+ merged.push(target);
182
+ }
183
+ return merged;
184
+ }
185
+
186
+ export function normalizeConfigTarget(raw) {
187
+ const out = {
188
+ id: String(raw.id).trim(),
189
+ provider: raw.provider,
190
+ selector: { ...raw.selector },
191
+ provenance: raw.provenance ?? 'config',
192
+ };
193
+
194
+ if (out.provider === 'jira' && out.selector.project) {
195
+ out.selector.project = String(out.selector.project).trim().toUpperCase();
196
+ }
197
+ if (out.provider === 'github' && out.selector.repo) {
198
+ out.selector.repo = String(out.selector.repo).trim();
199
+ }
200
+ if (out.provider === 'linear' && out.selector.team) {
201
+ out.selector.team = String(out.selector.team).trim();
202
+ }
203
+ if (out.provider === 'slack' && out.selector.channel) {
204
+ out.selector.channel = String(out.selector.channel).trim().replace(/^#/, '');
205
+ if (!out.selector.intent) out.selector.intent = 'internal';
206
+ }
207
+ return out;
208
+ }
209
+
210
+ export function resolveEffectiveSourceTargetsFromConfig(config, env = process.env) {
211
+ const configTargets = (config?.sources?.targets ?? []).map(normalizeConfigTarget);
212
+ const envTargets = legacyEnvSourceTargets(env);
213
+ return mergeSourceTargets(configTargets, envTargets);
214
+ }
215
+
216
+ export function targetsToEmbedSources(targets) {
217
+ const sources = [];
218
+ const githubRepos = targets.filter((t) => t.provider === 'github').map((t) => t.selector.repo);
219
+ if (githubRepos.length) {
220
+ sources.push({
221
+ provider: 'github',
222
+ repos: githubRepos,
223
+ refs: ['prs', 'issues', 'commits'],
224
+ limit: 25,
225
+ });
226
+ sources.push({
227
+ provider: 'github',
228
+ repos: githubRepos,
229
+ refs: ['meta', 'readme', 'docs'],
230
+ limit: 25,
231
+ });
232
+ }
233
+
234
+ for (const target of targets.filter((t) => t.provider === 'jira')) {
235
+ sources.push({
236
+ provider: 'jira',
237
+ project: target.selector.project,
238
+ refs: ['issues'],
239
+ limit: 50,
240
+ });
241
+ }
242
+
243
+ for (const target of targets.filter((t) => t.provider === 'linear')) {
244
+ sources.push({
245
+ provider: 'linear',
246
+ team: target.selector.team,
247
+ refs: ['issues'],
248
+ limit: 50,
249
+ });
250
+ }
251
+
252
+ const slackByIntent = new Map();
253
+ for (const target of targets.filter((t) => t.provider === 'slack')) {
254
+ const intent = target.selector.intent ?? 'internal';
255
+ if (!slackByIntent.has(intent)) slackByIntent.set(intent, []);
256
+ slackByIntent.get(intent).push(target.selector.channel);
257
+ }
258
+ for (const [intent, channels] of slackByIntent) {
259
+ sources.push({
260
+ provider: 'slack',
261
+ channels,
262
+ intent,
263
+ refs: ['messages'],
264
+ oldest: 86400,
265
+ limit: 50,
266
+ });
267
+ }
268
+
269
+ return sources;
270
+ }
271
+
272
+ export function resolveKnownSourcesFromTargets(targets) {
273
+ const sources = [];
274
+
275
+ for (const target of targets) {
276
+ const sel = target.selector ?? {};
277
+ if (target.provider === 'github' && sel.repo) {
278
+ const repo = sel.repo;
279
+ const short = repo.split('/').pop();
280
+ sources.push({ id: repo.toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
281
+ sources.push({ id: short.toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
282
+ sources.push({ id: short.replace(/-/g, '').toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
283
+ const words = short.split('-');
284
+ if (words.length > 1) {
285
+ sources.push({ id: words[words.length - 1].toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
286
+ }
287
+ }
288
+ if (target.provider === 'jira' && sel.project) {
289
+ const key = sel.project;
290
+ sources.push({ id: key.toLowerCase(), provider: 'jira', ref: key, display: `Jira/${key}`, targetId: target.id });
291
+ }
292
+ if (target.provider === 'linear' && sel.team) {
293
+ const team = sel.team;
294
+ sources.push({ id: team.toLowerCase(), provider: 'linear', ref: team, display: `Linear/${team}`, targetId: target.id });
295
+ }
296
+ if (target.provider === 'slack' && sel.channel) {
297
+ const ch = sel.channel;
298
+ sources.push({ id: ch.toLowerCase(), provider: 'slack', ref: ch, display: `Slack/${ch}`, targetId: target.id });
299
+ }
300
+ }
301
+
302
+ const providers = new Set(targets.map((t) => t.provider));
303
+ if (providers.has('jira')) {
304
+ sources.push({ id: 'jira', provider: 'jira', ref: null, display: 'Jira' });
305
+ }
306
+ if (providers.has('linear')) {
307
+ sources.push({ id: 'linear', provider: 'linear', ref: null, display: 'Linear' });
308
+ }
309
+
310
+ return sources;
311
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * lib/doctor/source-checkout.mjs — distinguish Construct source tree vs npm install.
3
+ *
4
+ * `isConstructPackageRepo` is true for both checkouts and consumer installs.
5
+ * Source-only doctor checks (dashboard static export, certification role cards)
6
+ * should run only when this returns true.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ export function isConstructSourceCheckout(rootDir) {
13
+ const root = path.resolve(rootDir);
14
+ return fs.existsSync(path.join(root, 'apps', 'dashboard'))
15
+ || fs.existsSync(path.join(root, 'tests', 'certification', 'specialists'));
16
+ }
@@ -27,6 +27,7 @@ import {
27
27
  import { exportDeckPptx, pptxgenPresent } from './deck-export-pptx.mjs';
28
28
  import { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
29
29
  import { convertDocxToDoc, libreOfficeInstallHint, libreOfficePresent, resolveLibreOfficeBin } from './libreoffice-export.mjs';
30
+ import { resolveExportBranding } from './export-branding.mjs';
30
31
 
31
32
  export const EXPORT_FORMATS = ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'];
32
33
 
@@ -153,6 +154,7 @@ function installHint(name) {
153
154
  if (name === 'mmdc') return 'Install mermaid-cli for mermaid blocks (`npm install -g @mermaid-js/mermaid-cli`).';
154
155
  if (name === 'pptxgenjs') return 'Install pptxgenjs to enable PPTX export (`npm install pptxgenjs` in the Construct package).';
155
156
  if (name === 'construct-deck.html') return 'Bundled deck template missing from templates/distribution/construct-deck.html (Construct install integrity issue).';
157
+ if (name === 'construct-reference.docx') return 'Bundled branded reference doc missing from templates/distribution/construct-reference.docx; construct-branded DOCX export degrades to pandoc default styling. Regenerate from a source checkout with `node scripts/build-reference-docx.mjs`.';
156
158
  if (name === 'libreoffice') return libreOfficeInstallHint();
157
159
  return `Install ${name} to enable this export format.`;
158
160
  }
@@ -160,13 +162,17 @@ function installHint(name) {
160
162
  export function detect(format, env = process.env, {
161
163
  figures = false,
162
164
  artifactType = null,
165
+ branding = 'construct',
163
166
  cwd = process.cwd(),
164
167
  repoRoot = REPO_ROOT,
165
168
  } = {}) {
166
- const config = FORMAT_ENGINES[format];
167
- if (!config) return { ok: false, format, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
169
+ const brandingPolicy = resolveExportBranding(format, branding);
170
+ const config = format === 'pptx' && brandingPolicy.applied === 'plain'
171
+ ? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
172
+ : FORMAT_ENGINES[format];
173
+ if (!config) return { ok: false, format, branding: brandingPolicy, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
168
174
  if (config.engine === 'copy') {
169
- return { ok: true, format, figures: false, present: true, binaries: [], missing: [], message: `Ready: source emit (${format})` };
175
+ return { ok: true, format, branding: brandingPolicy, figures: false, present: true, binaries: [], missing: [], message: `Ready: source emit (${format})` };
170
176
  }
171
177
  const required = [];
172
178
  if (config.engine === 'pptxgenjs') {
@@ -174,13 +180,19 @@ export function detect(format, env = process.env, {
174
180
  } else {
175
181
  required.push(config.engine, ...(config.extraBinaries || []));
176
182
  }
177
- if (format === 'pdf') {
183
+ if (format === 'pdf' && brandingPolicy.applied === 'construct') {
178
184
  const template = detectPdfTemplate({ artifactType, cwd, repoRoot });
179
185
  if (!template.present) required.push('construct-pdf.typ');
180
186
  }
181
- if (format === 'deck' && !deckTemplatePath(repoRoot)) {
187
+ if (format === 'deck' && brandingPolicy.applied === 'construct' && !deckTemplatePath(repoRoot)) {
182
188
  required.push('construct-deck.html');
183
189
  }
190
+ if (format === 'html' && brandingPolicy.applied === 'construct' && !htmlTemplatePath(repoRoot)) required.push('construct-web.html');
191
+
192
+ // The DOCX reference doc only restyles pandoc output; pandoc emits a valid file without it and
193
+ // the export passes --reference-doc only when present, so unlike the PDF/HTML/deck templates a
194
+ // missing reference degrades gracefully rather than blocking the export.
195
+
184
196
  if (format === 'doc' && !libreOfficePresent(env)) {
185
197
  required.push('libreoffice');
186
198
  }
@@ -193,9 +205,10 @@ export function detect(format, env = process.env, {
193
205
  if (name.endsWith('.lua')) {
194
206
  return { name, path: fs.existsSync(diagramFilterPath()) ? diagramFilterPath() : null, version: null };
195
207
  }
196
- if (name.endsWith('.html')) {
208
+ if (name === 'construct-deck.html') {
197
209
  return { name, path: deckTemplatePath(repoRoot), version: null };
198
210
  }
211
+ if (name === 'construct-web.html') return { name, path: htmlTemplatePath(repoRoot), version: null };
199
212
  if (name === 'pptxgenjs') {
200
213
  return { name, path: pptxgenPresent() ? 'bundled' : null, version: null };
201
214
  }
@@ -213,6 +226,7 @@ export function detect(format, env = process.env, {
213
226
  return {
214
227
  ok: true,
215
228
  format,
229
+ branding: brandingPolicy,
216
230
  figures,
217
231
  present: missing.length === 0,
218
232
  binaries: status,
@@ -290,6 +304,7 @@ export function exportMarkdown({
290
304
  outputPath,
291
305
  format,
292
306
  figures = false,
307
+ branding = 'construct',
293
308
  artifactType = null,
294
309
  env = process.env,
295
310
  spawnFn = spawnSync,
@@ -304,21 +319,24 @@ export function exportMarkdown({
304
319
  const resolvedType = artifactType || metadata.artifactType || null;
305
320
  const effectiveEnv = figures ? buildDistributionDiagramEnv(env) : env;
306
321
 
307
- const detection = detect(format, effectiveEnv, { figures, artifactType: resolvedType, cwd, repoRoot });
322
+ const brandingPolicy = resolveExportBranding(format, branding);
323
+ const detection = detect(format, effectiveEnv, { figures, artifactType: resolvedType, branding, cwd, repoRoot });
308
324
  if (!detection.present) {
309
325
  return { ok: false, format, inputPath, missing: detection.missing, message: detection.message };
310
326
  }
311
327
 
312
- const config = FORMAT_ENGINES[format];
328
+ const config = format === 'pptx' && brandingPolicy.applied === 'plain'
329
+ ? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
330
+ : FORMAT_ENGINES[format];
313
331
  const target = path.resolve(outputPath || defaultOutputPath(inputPath, format));
314
332
  fs.mkdirSync(path.dirname(target), { recursive: true });
315
333
 
316
334
  if (config.engine === 'copy') {
317
- return exportSourceCopy({ inputPath, target, format });
335
+ return { ...exportSourceCopy({ inputPath, target, format }), branding: brandingPolicy };
318
336
  }
319
337
 
320
- if (format === 'pptx') {
321
- return exportDeckPptx({ inputPath, outputPath: target, metadata, repoRoot });
338
+ if (format === 'pptx' && brandingPolicy.applied === 'construct') {
339
+ return { ...exportDeckPptx({ inputPath, outputPath: target, metadata, repoRoot }), branding: brandingPolicy };
322
340
  }
323
341
 
324
342
  if (format === 'doc') {
@@ -328,6 +346,7 @@ export function exportMarkdown({
328
346
  outputPath: intermediate,
329
347
  format: 'docx',
330
348
  figures,
349
+ branding,
331
350
  artifactType: resolvedType,
332
351
  env: effectiveEnv,
333
352
  spawnFn,
@@ -356,12 +375,13 @@ export function exportMarkdown({
356
375
  pdfEngine: null,
357
376
  figures,
358
377
  template: null,
378
+ branding: brandingPolicy,
359
379
  message: loResult.message,
360
380
  };
361
381
  }
362
382
 
363
383
  const prepared = prepareExportInput(inputPath, { format, figures, metadata });
364
- const templatePath = format === 'pdf'
384
+ const templatePath = format === 'pdf' && brandingPolicy.applied === 'construct'
365
385
  ? resolvePdfTemplatePath({ artifactType: resolvedType, cwd, repoRoot })
366
386
  : null;
367
387
 
@@ -378,25 +398,26 @@ export function exportMarkdown({
378
398
  });
379
399
 
380
400
  const args = ['-f', 'markdown', '-o', target, '--standalone'];
401
+ if (config.writer) args.push('-t', config.writer);
381
402
  if (config.pdfEngine) args.push(`--pdf-engine=${config.pdfEngine}`);
382
403
  if (format === 'pdf') {
383
404
  if (templatePath) args.push('--template', templatePath);
384
- args.push(...pdfEngineFontOpts(repoRoot));
405
+ if (brandingPolicy.applied === 'construct') args.push(...pdfEngineFontOpts(repoRoot));
385
406
  args.push(...metadataArgs);
386
407
  } else if (format === 'html') {
387
408
  const htmlTpl = htmlTemplatePath(repoRoot);
388
- if (htmlTpl) args.push('--template', htmlTpl);
409
+ if (htmlTpl && brandingPolicy.applied === 'construct') args.push('--template', htmlTpl);
389
410
  args.push('--embed-resources');
390
411
  args.push(...metadataArgs);
391
412
  } else if (format === 'deck') {
392
413
  const deckTpl = deckTemplatePath(repoRoot);
393
- if (deckTpl) args.push('--template', deckTpl);
414
+ if (deckTpl && brandingPolicy.applied === 'construct') args.push('--template', deckTpl);
394
415
  args.push('--section-divs');
395
416
  args.push('--embed-resources');
396
417
  args.push(...metadataArgs);
397
418
  } else if (format === 'docx') {
398
419
  const ref = docxReferencePath(repoRoot);
399
- if (ref) args.push('--reference-doc', ref);
420
+ if (ref && brandingPolicy.applied === 'construct') args.push('--reference-doc', ref);
400
421
  args.push(...metadataArgs);
401
422
  }
402
423
  if (figures && fs.existsSync(diagramFilterPath())) {
@@ -443,6 +464,7 @@ export function exportMarkdown({
443
464
  pdfEngine: config.pdfEngine,
444
465
  figures,
445
466
  template: templatePath,
467
+ branding: brandingPolicy,
446
468
  message: `Wrote ${path.relative(process.cwd(), target)}`,
447
469
  };
448
470
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * lib/embed/auto-sources.mjs — resolve embed auto-discovery sources from project config.
3
+ *
4
+ * When embed.yaml is absent, builds provider source records from typed
5
+ * construct.config.json targets merged with legacy env lists. Falls back
6
+ * to broad provider defaults only when no targets are configured.
7
+ */
8
+
9
+ import { loadProjectConfig } from '../config/project-config.mjs';
10
+ import {
11
+ resolveEffectiveSourceTargetsFromConfig,
12
+ targetsToEmbedSources,
13
+ legacyEnvSourceTargets,
14
+ } from '../config/source-targets.mjs';
15
+
16
+ export function resolveAutoEmbedSources({ cwd = process.cwd(), env = process.env, registry }) {
17
+ const { config } = loadProjectConfig(cwd, env);
18
+ const targets = resolveEffectiveSourceTargetsFromConfig(config, env);
19
+
20
+ if (targets.length === 0) {
21
+ return registry.autoSources(env);
22
+ }
23
+
24
+ const configuredProviders = new Set(targets.map((t) => t.provider));
25
+ const hasLegacyForUnconfigured = (provider) =>
26
+ legacyEnvSourceTargets(env).some((t) => t.provider === provider);
27
+
28
+ const sources = targetsToEmbedSources(targets);
29
+
30
+ for (const name of registry.names()) {
31
+ if (configuredProviders.has(name)) continue;
32
+ if (hasLegacyForUnconfigured(name)) continue;
33
+ const instance = registry.get(name);
34
+ if (typeof instance?.defaultSources !== 'function') continue;
35
+ }
36
+
37
+ const seen = new Set();
38
+ return sources.filter((s) => {
39
+ const key = `${s.provider}:${(s.refs ?? []).join(',')}:${JSON.stringify(s.repos ?? s.project ?? s.team ?? s.channels ?? '')}`;
40
+ if (seen.has(key)) return false;
41
+ seen.add(key);
42
+ return true;
43
+ });
44
+ }
package/lib/embed/cli.mjs CHANGED
@@ -401,7 +401,8 @@ async function cmdEmbedSnapshot(args, { homeDir = os.homedir() } = {}) {
401
401
  if (fs.existsSync(configPath)) {
402
402
  cfg = loadEmbedConfig(configPath);
403
403
  } else {
404
- cfg = { ...EMPTY_CONFIG, sources: registry.autoSources(env) };
404
+ const { resolveAutoEmbedSources } = await import('./auto-sources.mjs');
405
+ cfg = { ...EMPTY_CONFIG, sources: resolveAutoEmbedSources({ cwd: process.cwd(), env, registry }) };
405
406
  }
406
407
 
407
408
  if (!cfg.sources.length) {