genesis-compiler 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +443 -0
- package/bin/genesis.js +15 -0
- package/docs/assurance-model.md +26 -0
- package/docs/prompt-integration.md +98 -0
- package/docs/stack-components.md +304 -0
- package/package.json +57 -7
- package/plugins/genesis/.codex-plugin/plugin.json +19 -0
- package/plugins/genesis/hooks.json +18 -0
- package/prompts/blueprint.txt +9 -0
- package/prompts/describe.txt +17 -0
- package/prompts/deslop.txt +14 -0
- package/prompts/program.txt +12 -0
- package/prompts/reconcile.txt +12 -0
- package/prompts/review.txt +12 -0
- package/prompts/start.txt +30 -0
- package/prompts/work.txt +28 -0
- package/skills/genesis-deslop/SKILL.md +36 -0
- package/skills/genesis-deslop/agents/openai.yaml +4 -0
- package/skills/genesis-program/SKILL.md +66 -0
- package/skills/genesis-program/agents/openai.yaml +4 -0
- package/skills/genesis-project/SKILL.md +53 -0
- package/skills/genesis-project/agents/openai.yaml +4 -0
- package/src/cli.js +276 -0
- package/src/index/agent-skills.js +425 -0
- package/src/index/assets.js +19 -0
- package/src/index/blueprint.js +38 -0
- package/src/index/check.js +102 -0
- package/src/index/code-index.js +283 -0
- package/src/index/code-indexers/ast-grep.js +414 -0
- package/src/index/codex-hooks.js +367 -0
- package/src/index/codex-plugin.js +73 -0
- package/src/index/context.js +137 -0
- package/src/index/environment-files.js +19 -0
- package/src/index/errors.js +26 -0
- package/src/index/git.js +26 -0
- package/src/index/init.js +48 -0
- package/src/index/launch.js +34 -0
- package/src/index/paths.js +10 -0
- package/src/index/process.js +89 -0
- package/src/index/program.js +181 -0
- package/src/index/project-files.js +24 -0
- package/src/index/project-state.js +87 -0
- package/src/index/prompt.js +347 -0
- package/src/index/stack-catalog.js +72 -0
- package/src/index/stack-command.js +65 -0
- package/src/index/stack-composition.js +38 -0
- package/src/index/stack-environment-files.js +83 -0
- package/src/index/stack-launch.js +428 -0
- package/src/index/stack-piece.js +283 -0
- package/src/index/stack-preflight.js +25 -0
- package/src/index/stack-process.js +25 -0
- package/src/index/stack-workspace-setup.js +129 -0
- package/src/index/stack.js +302 -0
- package/src/index/utils.js +85 -0
- package/src/index/verification.js +77 -0
- package/src/index/workspace-setup.js +55 -0
- package/src/index.js +102 -0
- package/stacks/pieces/cpp.md +22 -0
- package/stacks/pieces/csharp.md +22 -0
- package/stacks/pieces/go.md +22 -0
- package/stacks/pieces/java.md +22 -0
- package/stacks/pieces/jskit-mysql.md +37 -0
- package/stacks/pieces/jskit.md +70 -0
- package/stacks/pieces/kotlin.md +22 -0
- package/stacks/pieces/mysql.md +18 -0
- package/stacks/pieces/nodejs.md +25 -0
- package/stacks/pieces/php.md +23 -0
- package/stacks/pieces/python.md +23 -0
- package/stacks/pieces/ruby.md +22 -0
- package/stacks/pieces/rust.md +22 -0
- package/stacks/pieces/shell.md +23 -0
- package/stacks/pieces/vue.md +19 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
isCanonicalProjectWorkdir,
|
|
4
|
+
isSafeProcessExecutable,
|
|
5
|
+
parseBacktickedArguments,
|
|
6
|
+
} from './stack-process.js';
|
|
7
|
+
|
|
8
|
+
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
9
|
+
const PLACEHOLDER = /\{([a-z][a-z0-9-]*)\}/gu;
|
|
10
|
+
const PREVIEW_IDENTITY_COMMAND_PATH = /^\.vibe64\/bin\/[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
11
|
+
const PREVIEW_IDENTITY_ENVIRONMENT_NAME = /^[A-Z_][A-Z0-9_]*$/u;
|
|
12
|
+
const PREVIEW_IDENTITY_PROTOCOL = 'vibe64.preview-identity.command.v1';
|
|
13
|
+
const PREVIEW_IDENTITY_RESERVED_ENVIRONMENT_NAMES = new Set([
|
|
14
|
+
'HOME',
|
|
15
|
+
'LOGNAME',
|
|
16
|
+
'NODE_OPTIONS',
|
|
17
|
+
'PATH',
|
|
18
|
+
'TMPDIR',
|
|
19
|
+
'USER',
|
|
20
|
+
'VIBE64_PREVIEW_IDENTITY_ENABLED',
|
|
21
|
+
'VIBE64_PREVIEW_IDENTITY_SECRET',
|
|
22
|
+
]);
|
|
23
|
+
const PREVIEW_IDENTITY_TYPES = new Set(['email', 'login', 'user-id']);
|
|
24
|
+
const PREVIEW_FIELDS = [
|
|
25
|
+
['- Command: ', 'command', 'Command'],
|
|
26
|
+
['- Protocol: ', 'protocol', 'Protocol'],
|
|
27
|
+
['- Identity types: ', 'identityTypes', 'Identity types'],
|
|
28
|
+
['- Enabled environment: ', 'enabledEnvironment', 'Enabled environment'],
|
|
29
|
+
['- Secret environment: ', 'secretEnvironment', 'Secret environment'],
|
|
30
|
+
['- Runtimes: ', 'runtimes', 'Runtimes'],
|
|
31
|
+
['- Timeout ms: ', 'timeoutMs', 'Timeout ms'],
|
|
32
|
+
];
|
|
33
|
+
const TARGET_HEADING = /^### Target `([^`\r\n]+)`:[ \t]+(.+)$/u;
|
|
34
|
+
|
|
35
|
+
function invalid(launchPath, message, line, details = {}) {
|
|
36
|
+
throw new GenesisError('STACK_LAUNCH_INVALID', message, {
|
|
37
|
+
path: launchPath,
|
|
38
|
+
...(line === undefined ? {} : { line }),
|
|
39
|
+
...details,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function text(value, launchPath, label, line) {
|
|
44
|
+
if (typeof value !== 'string' || !value.trim() || /[\0\r\n]/u.test(value)) {
|
|
45
|
+
invalid(launchPath, `${label} must be one non-empty line.`, line);
|
|
46
|
+
}
|
|
47
|
+
return value.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function id(value, launchPath, label, line) {
|
|
51
|
+
const normalized = text(value, launchPath, label, line);
|
|
52
|
+
if (!ID_PATTERN.test(normalized)) {
|
|
53
|
+
invalid(launchPath, `${label} must use lowercase letters, digits, and single hyphens.`, line);
|
|
54
|
+
}
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function tokenValues(source, prefix, launchPath, label, line) {
|
|
59
|
+
const values = parseBacktickedArguments(source.slice(prefix.length));
|
|
60
|
+
if (!values) {
|
|
61
|
+
invalid(launchPath, `${label} must contain separate non-empty backticked values.`, line);
|
|
62
|
+
}
|
|
63
|
+
return values;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function oneToken(source, prefix, launchPath, label, line) {
|
|
67
|
+
const values = tokenValues(source, prefix, launchPath, label, line);
|
|
68
|
+
if (values.length !== 1) invalid(launchPath, `${label} accepts exactly one value.`, line);
|
|
69
|
+
return values[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runtimeRequirements(values, launchPath, label, line, { deduplicate = false } = {}) {
|
|
73
|
+
const requirements = values.map((entry) => id(entry, launchPath, label, line));
|
|
74
|
+
if (!deduplicate && new Set(requirements).size !== requirements.length) {
|
|
75
|
+
invalid(launchPath, `${label} contains a duplicate technology id.`, line);
|
|
76
|
+
}
|
|
77
|
+
return deduplicate ? [...new Set(requirements)] : requirements;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeLaunchArgv(values, launchPath, line) {
|
|
81
|
+
if (!values || values.some((entry) => entry.includes('\0'))) {
|
|
82
|
+
invalid(
|
|
83
|
+
launchPath,
|
|
84
|
+
'Every launch command and argument must be one separate non-empty backticked value.',
|
|
85
|
+
line,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
for (const entry of values) {
|
|
89
|
+
if ([...entry.matchAll(PLACEHOLDER)].some((match) => !['host', 'port'].includes(match[1]))) {
|
|
90
|
+
invalid(launchPath, 'Launch argv supports only {host} and {port} placeholders.', line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!isSafeProcessExecutable(values[0])) {
|
|
94
|
+
invalid(
|
|
95
|
+
launchPath,
|
|
96
|
+
'A launch executable must be a PATH name or project-relative path without traversal.',
|
|
97
|
+
line,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return values;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function launchArgv(source, launchPath, line) {
|
|
104
|
+
return normalizeLaunchArgv(parseBacktickedArguments(source), launchPath, line);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setOnce(draft, field, value, launchPath, line, label) {
|
|
108
|
+
if (draft.seen.has(field)) invalid(launchPath, `Duplicate ${label}.`, line);
|
|
109
|
+
draft.seen.add(field);
|
|
110
|
+
draft[field] = value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function targetDraft(match, launchPath, line) {
|
|
114
|
+
return {
|
|
115
|
+
id: id(match[1], launchPath, 'Launch target id', line),
|
|
116
|
+
label: text(match[2], launchPath, 'Launch target label', line),
|
|
117
|
+
default: false,
|
|
118
|
+
workdir: '.',
|
|
119
|
+
preferredPort: null,
|
|
120
|
+
urlPath: '/',
|
|
121
|
+
runtimeRequirements: [],
|
|
122
|
+
steps: [],
|
|
123
|
+
previewIdentity: null,
|
|
124
|
+
seen: new Set(),
|
|
125
|
+
line,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePreferredPort(value, launchPath, line) {
|
|
130
|
+
if (!/^[0-9]+$/u.test(value)) {
|
|
131
|
+
invalid(launchPath, 'Launch target preferred port must be an integer from 1024 through 65535.', line);
|
|
132
|
+
}
|
|
133
|
+
const port = Number(value);
|
|
134
|
+
if (!Number.isSafeInteger(port) || port < 1024 || port > 65_535) {
|
|
135
|
+
invalid(launchPath, 'Launch target preferred port must be an integer from 1024 through 65535.', line);
|
|
136
|
+
}
|
|
137
|
+
return port;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseUrlPath(value, launchPath, line) {
|
|
141
|
+
const normalized = text(value, launchPath, 'Launch target URL path', line);
|
|
142
|
+
if (!normalized.startsWith('/') || normalized.startsWith('//') || /[\\?#]/u.test(normalized)) {
|
|
143
|
+
invalid(
|
|
144
|
+
launchPath,
|
|
145
|
+
'Launch target URL path must be an application path beginning with one slash.',
|
|
146
|
+
line,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseTargetLine(draft, source, launchPath, line) {
|
|
153
|
+
if (source === '- Default.') {
|
|
154
|
+
setOnce(draft, 'default', true, launchPath, line, 'Launch target Default entry');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (source.startsWith('- Workdir: ')) {
|
|
158
|
+
const value = oneToken(source, '- Workdir: ', launchPath, 'Launch target Workdir', line);
|
|
159
|
+
if (!isCanonicalProjectWorkdir(value)) {
|
|
160
|
+
invalid(launchPath, 'Launch target workdir must be a canonical project-relative directory.', line);
|
|
161
|
+
}
|
|
162
|
+
setOnce(draft, 'workdir', value, launchPath, line, 'Launch target Workdir entry');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (source.startsWith('- Preferred port: ')) {
|
|
166
|
+
const value = oneToken(
|
|
167
|
+
source,
|
|
168
|
+
'- Preferred port: ',
|
|
169
|
+
launchPath,
|
|
170
|
+
'Launch target Preferred port',
|
|
171
|
+
line,
|
|
172
|
+
);
|
|
173
|
+
setOnce(
|
|
174
|
+
draft,
|
|
175
|
+
'preferredPort',
|
|
176
|
+
parsePreferredPort(value, launchPath, line),
|
|
177
|
+
launchPath,
|
|
178
|
+
line,
|
|
179
|
+
'Launch target Preferred port entry',
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (source.startsWith('- URL path: ')) {
|
|
184
|
+
const value = oneToken(source, '- URL path: ', launchPath, 'Launch target URL path', line);
|
|
185
|
+
setOnce(
|
|
186
|
+
draft,
|
|
187
|
+
'urlPath',
|
|
188
|
+
parseUrlPath(value, launchPath, line),
|
|
189
|
+
launchPath,
|
|
190
|
+
line,
|
|
191
|
+
'Launch target URL path entry',
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (source.startsWith('- Runtimes: ')) {
|
|
196
|
+
const values = tokenValues(source, '- Runtimes: ', launchPath, 'Launch target Runtimes', line);
|
|
197
|
+
setOnce(
|
|
198
|
+
draft,
|
|
199
|
+
'runtimeRequirements',
|
|
200
|
+
runtimeRequirements(values, launchPath, 'Launch target Runtimes', line),
|
|
201
|
+
launchPath,
|
|
202
|
+
line,
|
|
203
|
+
'Launch target Runtimes entry',
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const step = source.match(/^- (Prepare|Serve) `([^`\r\n]+)`:[ \t]+(.+)$/u);
|
|
208
|
+
if (step) {
|
|
209
|
+
draft.steps.push({
|
|
210
|
+
label: text(step[2], launchPath, 'Launch step label', line),
|
|
211
|
+
argv: launchArgv(step[3], launchPath, line),
|
|
212
|
+
role: step[1] === 'Serve' ? 'server' : 'prepare',
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
invalid(launchPath, `Unknown Launch target entry: ${source}.`, line);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function previewIdentityEnvironmentName(value, launchPath, label, line) {
|
|
220
|
+
if (value === undefined) return '';
|
|
221
|
+
const name = text(value, launchPath, `Preview identity ${label} environment name`, line);
|
|
222
|
+
if (
|
|
223
|
+
!PREVIEW_IDENTITY_ENVIRONMENT_NAME.test(name)
|
|
224
|
+
|| PREVIEW_IDENTITY_RESERVED_ENVIRONMENT_NAMES.has(name)
|
|
225
|
+
|| name.startsWith('XDG_')
|
|
226
|
+
) {
|
|
227
|
+
invalid(launchPath, `Preview identity ${label} environment name is invalid.`, line);
|
|
228
|
+
}
|
|
229
|
+
return name;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function previewDraft(line) {
|
|
233
|
+
return { seen: new Set(), line };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parsePreviewLine(draft, source, launchPath, line) {
|
|
237
|
+
const field = PREVIEW_FIELDS.find(([prefix]) => source.startsWith(prefix));
|
|
238
|
+
if (!field) invalid(launchPath, `Unknown Preview identity entry: ${source}.`, line);
|
|
239
|
+
const [prefix, key, label] = field;
|
|
240
|
+
const values = tokenValues(source, prefix, launchPath, `Preview identity ${label}`, line);
|
|
241
|
+
if (!['command', 'identityTypes', 'runtimes'].includes(key) && values.length !== 1) {
|
|
242
|
+
invalid(launchPath, `Preview identity ${label} accepts exactly one value.`, line);
|
|
243
|
+
}
|
|
244
|
+
setOnce(draft, key, values, launchPath, line, `Preview identity ${label} entry`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizePreviewIdentity(draft, launchPath, targetId) {
|
|
248
|
+
if (!draft.command) {
|
|
249
|
+
invalid(launchPath, `Launch target ${targetId} Preview identity needs Command.`, draft.line);
|
|
250
|
+
}
|
|
251
|
+
if (!draft.protocol) {
|
|
252
|
+
invalid(launchPath, `Launch target ${targetId} Preview identity needs Protocol.`, draft.line);
|
|
253
|
+
}
|
|
254
|
+
if (!draft.identityTypes) {
|
|
255
|
+
invalid(launchPath, `Launch target ${targetId} Preview identity needs Identity types.`, draft.line);
|
|
256
|
+
}
|
|
257
|
+
const command = normalizeLaunchArgv(draft.command, launchPath, draft.line);
|
|
258
|
+
if (
|
|
259
|
+
command.length > 64
|
|
260
|
+
|| command.some((entry) => !entry.trim() || entry.length > 4096 || /[\r\n]/u.test(entry))
|
|
261
|
+
|| !PREVIEW_IDENTITY_COMMAND_PATH.test(command[0])
|
|
262
|
+
|| command.some((entry) => /\{(?:host|port)\}/u.test(entry))
|
|
263
|
+
) {
|
|
264
|
+
invalid(
|
|
265
|
+
launchPath,
|
|
266
|
+
'Preview identity command must use an app-owned executable under .vibe64/bin and contain at most 64 literal arguments.',
|
|
267
|
+
draft.line,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const protocol = text(draft.protocol[0], launchPath, 'Preview identity protocol', draft.line);
|
|
271
|
+
if (protocol !== PREVIEW_IDENTITY_PROTOCOL) {
|
|
272
|
+
invalid(launchPath, `Preview identity protocol must be ${PREVIEW_IDENTITY_PROTOCOL}.`, draft.line);
|
|
273
|
+
}
|
|
274
|
+
const identityTypes = [...new Set(draft.identityTypes.map((entry) => {
|
|
275
|
+
const type = text(entry, launchPath, 'Preview identity type', draft.line);
|
|
276
|
+
if (!PREVIEW_IDENTITY_TYPES.has(type)) {
|
|
277
|
+
invalid(launchPath, `Unsupported preview identity type: ${type}.`, draft.line);
|
|
278
|
+
}
|
|
279
|
+
return type;
|
|
280
|
+
}))];
|
|
281
|
+
const environment = {
|
|
282
|
+
enabled: previewIdentityEnvironmentName(
|
|
283
|
+
draft.enabledEnvironment?.[0],
|
|
284
|
+
launchPath,
|
|
285
|
+
'enabled',
|
|
286
|
+
draft.line,
|
|
287
|
+
),
|
|
288
|
+
secret: previewIdentityEnvironmentName(
|
|
289
|
+
draft.secretEnvironment?.[0],
|
|
290
|
+
launchPath,
|
|
291
|
+
'secret',
|
|
292
|
+
draft.line,
|
|
293
|
+
),
|
|
294
|
+
};
|
|
295
|
+
if (environment.enabled && environment.enabled === environment.secret) {
|
|
296
|
+
invalid(
|
|
297
|
+
launchPath,
|
|
298
|
+
'Preview identity enabled and secret environment names must differ.',
|
|
299
|
+
draft.line,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
let timeoutMs = 10_000;
|
|
303
|
+
if (draft.timeoutMs) {
|
|
304
|
+
const value = draft.timeoutMs[0];
|
|
305
|
+
if (!/^[0-9]+$/u.test(value)) {
|
|
306
|
+
invalid(launchPath, 'Preview identity Timeout ms must be an integer from 1 through 30000.', draft.line);
|
|
307
|
+
}
|
|
308
|
+
timeoutMs = Number(value);
|
|
309
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000) {
|
|
310
|
+
invalid(launchPath, 'Preview identity Timeout ms must be an integer from 1 through 30000.', draft.line);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
command,
|
|
315
|
+
environment,
|
|
316
|
+
identityTypes,
|
|
317
|
+
protocol,
|
|
318
|
+
runtimes: draft.runtimes
|
|
319
|
+
? runtimeRequirements(
|
|
320
|
+
draft.runtimes,
|
|
321
|
+
launchPath,
|
|
322
|
+
'Preview identity Runtimes',
|
|
323
|
+
draft.line,
|
|
324
|
+
{ deduplicate: true },
|
|
325
|
+
)
|
|
326
|
+
: [],
|
|
327
|
+
timeoutMs,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeTarget(draft, launchPath) {
|
|
332
|
+
if (draft.steps.length === 0) {
|
|
333
|
+
invalid(launchPath, `Launch target ${draft.id} needs at least one step.`, draft.line);
|
|
334
|
+
}
|
|
335
|
+
const serverSteps = draft.steps.filter(({ role }) => role === 'server');
|
|
336
|
+
if (serverSteps.length !== 1 || draft.steps.at(-1).role !== 'server') {
|
|
337
|
+
invalid(launchPath, `Launch target ${draft.id} needs exactly one final Serve step.`, draft.line);
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
id: draft.id,
|
|
341
|
+
label: draft.label,
|
|
342
|
+
default: draft.default,
|
|
343
|
+
workdir: draft.workdir,
|
|
344
|
+
preferredPort: draft.preferredPort,
|
|
345
|
+
urlPath: draft.urlPath,
|
|
346
|
+
runtimeRequirements: draft.runtimeRequirements,
|
|
347
|
+
steps: draft.steps,
|
|
348
|
+
...(draft.previewIdentity
|
|
349
|
+
? { previewIdentity: normalizePreviewIdentity(draft.previewIdentity, launchPath, draft.id) }
|
|
350
|
+
: {}),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function parseStackLaunchLines(lines, {
|
|
355
|
+
path: launchPath = 'stack launch',
|
|
356
|
+
} = {}) {
|
|
357
|
+
if (lines === undefined) return null;
|
|
358
|
+
const entries = lines.map((line) => line.trim()).filter(Boolean);
|
|
359
|
+
if (entries.length === 1 && entries[0] === '- Nothing.') return { version: 1, targets: [] };
|
|
360
|
+
|
|
361
|
+
const targets = [];
|
|
362
|
+
let target = null;
|
|
363
|
+
let inPreviewIdentity = false;
|
|
364
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
365
|
+
const source = lines[index].trim();
|
|
366
|
+
if (!source) continue;
|
|
367
|
+
const line = index + 1;
|
|
368
|
+
const heading = source.match(TARGET_HEADING);
|
|
369
|
+
if (heading) {
|
|
370
|
+
if (target) targets.push(normalizeTarget(target, launchPath));
|
|
371
|
+
target = targetDraft(heading, launchPath, line);
|
|
372
|
+
inPreviewIdentity = false;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (source === '#### Preview identity') {
|
|
376
|
+
if (!target || target.previewIdentity) {
|
|
377
|
+
invalid(launchPath, 'Preview identity must appear once below a Launch target.', line);
|
|
378
|
+
}
|
|
379
|
+
target.previewIdentity = previewDraft(line);
|
|
380
|
+
inPreviewIdentity = true;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (!target) {
|
|
384
|
+
invalid(launchPath, '## Launch must begin with a Target heading.', line);
|
|
385
|
+
}
|
|
386
|
+
if (inPreviewIdentity) parsePreviewLine(target.previewIdentity, source, launchPath, line);
|
|
387
|
+
else parseTargetLine(target, source, launchPath, line);
|
|
388
|
+
}
|
|
389
|
+
if (target) targets.push(normalizeTarget(target, launchPath));
|
|
390
|
+
if (targets.length === 0) {
|
|
391
|
+
invalid(launchPath, '## Launch needs at least one Target or exactly `- Nothing.`.');
|
|
392
|
+
}
|
|
393
|
+
const ids = targets.map(({ id: targetId }) => targetId);
|
|
394
|
+
if (new Set(ids).size !== ids.length) invalid(launchPath, 'Launch contains a duplicate target id.');
|
|
395
|
+
if (targets.filter(({ default: isDefault }) => isDefault).length > 1) {
|
|
396
|
+
invalid(launchPath, 'Launch may declare at most one default target.');
|
|
397
|
+
}
|
|
398
|
+
return { version: 1, targets };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function composeStackLaunchTargets(components, projectLaunch) {
|
|
402
|
+
if (projectLaunch) {
|
|
403
|
+
return projectLaunch.targets.map((target) => ({ ...target, source: 'project' }));
|
|
404
|
+
}
|
|
405
|
+
const targets = components.flatMap((component) => (
|
|
406
|
+
(component.launchTargets || []).map((target) => ({
|
|
407
|
+
...target,
|
|
408
|
+
source: `component:${component.id}`,
|
|
409
|
+
}))
|
|
410
|
+
));
|
|
411
|
+
const sourcesById = new Map();
|
|
412
|
+
for (const target of targets) {
|
|
413
|
+
const previous = sourcesById.get(target.id);
|
|
414
|
+
if (previous) {
|
|
415
|
+
invalid(
|
|
416
|
+
'selected Stack components',
|
|
417
|
+
`Selected Stack components provide duplicate launch target ${target.id}.`,
|
|
418
|
+
undefined,
|
|
419
|
+
{ sources: [previous, target.source], target: target.id },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
sourcesById.set(target.id, target.source);
|
|
423
|
+
}
|
|
424
|
+
if (targets.filter((target) => target.default).length > 1) {
|
|
425
|
+
invalid('selected Stack components', 'Selected Stack components provide more than one default launch target.');
|
|
426
|
+
}
|
|
427
|
+
return targets;
|
|
428
|
+
}
|