@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.
- package/README.md +2 -1
- package/bin/construct +180 -10
- package/lib/artifact-manifest.mjs +150 -4
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/auto-docs.mjs +3 -1
- package/lib/chat/present.mjs +1 -0
- package/lib/cli-commands.mjs +20 -3
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/config/intake-policy.mjs +209 -0
- package/lib/config/project-config.mjs +11 -2
- package/lib/config/schema.mjs +50 -1
- package/lib/config/source-targets.mjs +311 -0
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/document-export.mjs +38 -16
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +2 -1
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox.mjs +6 -10
- package/lib/embed/worker.mjs +2 -1
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/export-branding.mjs +32 -0
- package/lib/hooks/session-start.mjs +14 -11
- package/lib/init-unified.mjs +24 -23
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/intake-config.mjs +52 -105
- package/lib/intake/legacy-paths.mjs +5 -0
- package/lib/mcp/server.mjs +19 -1
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/playwright-demo.mjs +2 -0
- package/lib/server/index.mjs +31 -2
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +3 -2
- package/package.json +1 -1
- package/specialists/artifact-manifest.json +11 -2
- package/specialists/artifact-manifest.schema.json +31 -1
- package/templates/distribution/construct-reference.docx +0 -0
|
@@ -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
|
+
}
|
package/lib/document-export.mjs
CHANGED
|
@@ -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
|
|
167
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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) {
|