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,347 @@
|
|
|
1
|
+
import { readInstalledAsset } from './assets.js';
|
|
2
|
+
import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js';
|
|
3
|
+
import { buildProjectIndex, MACHINE_CITY_PATH, PROGRAM_CITY_PATH } from './code-index.js';
|
|
4
|
+
import { BLUEPRINT_SKELETON_SOURCE, readBlueprint } from './blueprint.js';
|
|
5
|
+
import { readStack, stackPromptContext } from './stack.js';
|
|
6
|
+
import { listBuiltinStackPieces } from './stack-catalog.js';
|
|
7
|
+
import { GenesisError } from './errors.js';
|
|
8
|
+
import { gitContext } from './git.js';
|
|
9
|
+
import { inspectProgram } from './program.js';
|
|
10
|
+
import { inspectVerification } from './project-state.js';
|
|
11
|
+
import { missingStackResources } from './stack-preflight.js';
|
|
12
|
+
import { stableJson } from './utils.js';
|
|
13
|
+
import { gitVisibleFileStates } from './project-files.js';
|
|
14
|
+
import { isProjectContentPath } from './paths.js';
|
|
15
|
+
|
|
16
|
+
const TASKS = new Set(['start', 'work', 'deslop', 'program', 'blueprint', 'describe', 'review']);
|
|
17
|
+
const DEFAULT_REQUEST = {
|
|
18
|
+
start: 'Start a conversation about this project.',
|
|
19
|
+
work: 'Implement the product intent expressed by the current Blueprint.',
|
|
20
|
+
deslop: 'Simplify the current Git-visible work without making unrelated changes.',
|
|
21
|
+
program: 'Refresh the complete useful Program for the code that exists now.',
|
|
22
|
+
describe: 'Create or refresh the complete Blueprint and useful Program for the codebase that exists now.',
|
|
23
|
+
review: 'Review the complete useful relationship between Blueprint, code, Program, and tests.',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function stackCatalogContext(pieces = []) {
|
|
27
|
+
return pieces.map(({ id, description, requires, conflicts }) => ({
|
|
28
|
+
id,
|
|
29
|
+
description,
|
|
30
|
+
requires,
|
|
31
|
+
conflicts,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requestText(value, task) {
|
|
36
|
+
const request = String(value ?? '').trim();
|
|
37
|
+
if (Buffer.byteLength(request, 'utf8') > 64 * 1024) {
|
|
38
|
+
throw new GenesisError('PROMPT_REQUEST_INVALID', 'Prompt request exceeds 64 KiB.');
|
|
39
|
+
}
|
|
40
|
+
if (task === 'blueprint' && !request) {
|
|
41
|
+
throw new GenesisError('PROMPT_REQUEST_REQUIRED', 'Blueprint prompt generation requires explicit user intent.');
|
|
42
|
+
}
|
|
43
|
+
return request || DEFAULT_REQUEST[task];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function observeProgram(projectRoot) {
|
|
47
|
+
try {
|
|
48
|
+
return await inspectProgram(projectRoot);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
status: 'invalid',
|
|
52
|
+
files: [],
|
|
53
|
+
modules: [],
|
|
54
|
+
subsystems: [],
|
|
55
|
+
diagnostic: { code: error.code || 'PROGRAM_INVALID', message: error.message },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function programContext(program) {
|
|
61
|
+
return {
|
|
62
|
+
status: program.status,
|
|
63
|
+
files: program.files,
|
|
64
|
+
subsystems: program.subsystems,
|
|
65
|
+
modules: program.modules.map(({ path, name, sources, subsystem }) => ({
|
|
66
|
+
path,
|
|
67
|
+
name,
|
|
68
|
+
sources,
|
|
69
|
+
subsystem,
|
|
70
|
+
})),
|
|
71
|
+
...(program.diagnostic ? { diagnostic: program.diagnostic } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function codeIndexContext(index) {
|
|
76
|
+
return {
|
|
77
|
+
machineCity: {
|
|
78
|
+
path: MACHINE_CITY_PATH,
|
|
79
|
+
status: index.machine.status,
|
|
80
|
+
files: index.fileCount,
|
|
81
|
+
functions: index.functionCount,
|
|
82
|
+
},
|
|
83
|
+
programCity: {
|
|
84
|
+
path: PROGRAM_CITY_PATH,
|
|
85
|
+
status: index.program.status,
|
|
86
|
+
operations: index.operationCount,
|
|
87
|
+
},
|
|
88
|
+
query: 'genesis index <function-or-path...>',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderPrompt({
|
|
93
|
+
instructions,
|
|
94
|
+
request,
|
|
95
|
+
context,
|
|
96
|
+
skills = '',
|
|
97
|
+
guidance = '',
|
|
98
|
+
cleanup = '',
|
|
99
|
+
}) {
|
|
100
|
+
return [
|
|
101
|
+
instructions.trim(),
|
|
102
|
+
'',
|
|
103
|
+
'USER REQUEST',
|
|
104
|
+
'',
|
|
105
|
+
request,
|
|
106
|
+
'',
|
|
107
|
+
'GENESIS CONTEXT',
|
|
108
|
+
'',
|
|
109
|
+
'```json',
|
|
110
|
+
stableJson(context).trimEnd(),
|
|
111
|
+
'```',
|
|
112
|
+
...(guidance ? ['', 'SELECTED STACK GUIDANCE', '', guidance] : []),
|
|
113
|
+
...(skills ? ['', 'AVAILABLE AGENT SKILLS', '', skills] : []),
|
|
114
|
+
...(cleanup ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', cleanup] : []),
|
|
115
|
+
'',
|
|
116
|
+
].join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function generateExplanationPrompt({ instructions, program, request, root, task }) {
|
|
120
|
+
const blueprint = await readBlueprint(root, {
|
|
121
|
+
required: true,
|
|
122
|
+
requireDescription: task === 'program',
|
|
123
|
+
});
|
|
124
|
+
const [stack, index] = await Promise.all([
|
|
125
|
+
readStack(root),
|
|
126
|
+
buildProjectIndex({ projectRoot: root, write: false }),
|
|
127
|
+
]);
|
|
128
|
+
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
129
|
+
const blueprintContext = task === 'describe'
|
|
130
|
+
? { path: blueprint.path, source: blueprint.source }
|
|
131
|
+
: { path: blueprint.path, description: blueprint.description };
|
|
132
|
+
return {
|
|
133
|
+
status: 'ready',
|
|
134
|
+
task,
|
|
135
|
+
prompt: renderPrompt({
|
|
136
|
+
instructions,
|
|
137
|
+
request,
|
|
138
|
+
context: {
|
|
139
|
+
task,
|
|
140
|
+
projectRoot: root,
|
|
141
|
+
blueprint: blueprintContext,
|
|
142
|
+
stack: stackPromptContext(stack),
|
|
143
|
+
program: programContext(program),
|
|
144
|
+
codeIndex: codeIndexContext(index),
|
|
145
|
+
},
|
|
146
|
+
guidance: stack.guidance,
|
|
147
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
148
|
+
}),
|
|
149
|
+
warnings: [
|
|
150
|
+
...(program.diagnostic ? [program.diagnostic] : []),
|
|
151
|
+
...projectSkills.diagnostics,
|
|
152
|
+
...index.diagnostics,
|
|
153
|
+
],
|
|
154
|
+
verificationCommands: [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function generateStartPrompt({ instructions, program, request, root }) {
|
|
159
|
+
let blueprint;
|
|
160
|
+
try {
|
|
161
|
+
blueprint = await readBlueprint(root, { required: true });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error?.code !== 'BLUEPRINT_REQUIRED') throw error;
|
|
164
|
+
const files = await gitVisibleFileStates(root, { includePath: isProjectContentPath });
|
|
165
|
+
const existing = [...files.values()].some((state) => state.exists);
|
|
166
|
+
if (!existing) throw error;
|
|
167
|
+
return {
|
|
168
|
+
status: 'ready',
|
|
169
|
+
task: 'start',
|
|
170
|
+
prompt: renderPrompt({
|
|
171
|
+
instructions,
|
|
172
|
+
request,
|
|
173
|
+
context: {
|
|
174
|
+
task: 'start',
|
|
175
|
+
projectRoot: root,
|
|
176
|
+
projectKind: 'existing-uninitialized',
|
|
177
|
+
genesis: { initialized: false },
|
|
178
|
+
program: programContext(program),
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
warnings: program.diagnostic ? [program.diagnostic] : [],
|
|
182
|
+
verificationCommands: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const [stack, index, catalog] = await Promise.all([
|
|
186
|
+
readStack(root),
|
|
187
|
+
buildProjectIndex({ projectRoot: root, write: false }),
|
|
188
|
+
listBuiltinStackPieces(),
|
|
189
|
+
]);
|
|
190
|
+
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
191
|
+
const newProject = stack.components.length === 0 && index.fileCount === 0;
|
|
192
|
+
return {
|
|
193
|
+
status: 'ready',
|
|
194
|
+
task: 'start',
|
|
195
|
+
prompt: renderPrompt({
|
|
196
|
+
instructions,
|
|
197
|
+
request,
|
|
198
|
+
context: {
|
|
199
|
+
task: 'start',
|
|
200
|
+
projectRoot: root,
|
|
201
|
+
projectKind: newProject ? 'new' : 'existing',
|
|
202
|
+
blueprint: {
|
|
203
|
+
path: blueprint.path,
|
|
204
|
+
description: blueprint.description,
|
|
205
|
+
},
|
|
206
|
+
stack: stackPromptContext(stack),
|
|
207
|
+
availableStackPieces: stackCatalogContext(catalog),
|
|
208
|
+
program: programContext(program),
|
|
209
|
+
codeIndex: codeIndexContext(index),
|
|
210
|
+
},
|
|
211
|
+
guidance: stack.guidance,
|
|
212
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
213
|
+
}),
|
|
214
|
+
warnings: [
|
|
215
|
+
...(program.diagnostic ? [program.diagnostic] : []),
|
|
216
|
+
...projectSkills.diagnostics,
|
|
217
|
+
...index.diagnostics,
|
|
218
|
+
],
|
|
219
|
+
verificationCommands: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function generateProjectPrompt({
|
|
224
|
+
environment = process.env,
|
|
225
|
+
projectRoot,
|
|
226
|
+
request = '',
|
|
227
|
+
task = 'work',
|
|
228
|
+
} = {}) {
|
|
229
|
+
if (!TASKS.has(task)) {
|
|
230
|
+
throw new GenesisError('PROMPT_TASK_INVALID', `Prompt task must be one of: ${[...TASKS].join(', ')}.`);
|
|
231
|
+
}
|
|
232
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
233
|
+
const userRequest = requestText(request, task);
|
|
234
|
+
const instructions = await readInstalledAsset(task);
|
|
235
|
+
|
|
236
|
+
if (task === 'blueprint') {
|
|
237
|
+
const blueprint = await readBlueprint(root);
|
|
238
|
+
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack: null });
|
|
239
|
+
const prompt = renderPrompt({
|
|
240
|
+
instructions,
|
|
241
|
+
request: userRequest,
|
|
242
|
+
context: {
|
|
243
|
+
task,
|
|
244
|
+
projectRoot: root,
|
|
245
|
+
blueprint: {
|
|
246
|
+
path: 'genesis/blueprint.md',
|
|
247
|
+
source: blueprint?.source || BLUEPRINT_SKELETON_SOURCE,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
status: 'ready',
|
|
254
|
+
task,
|
|
255
|
+
prompt,
|
|
256
|
+
warnings: projectSkills.diagnostics,
|
|
257
|
+
verificationCommands: [],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const program = await observeProgram(root);
|
|
262
|
+
if (task === 'start') {
|
|
263
|
+
return generateStartPrompt({
|
|
264
|
+
instructions,
|
|
265
|
+
program,
|
|
266
|
+
request: userRequest,
|
|
267
|
+
root,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
let stack;
|
|
271
|
+
if (task === 'work') {
|
|
272
|
+
try {
|
|
273
|
+
stack = await readStack(root);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error?.code !== 'STACK_REQUIRED') throw error;
|
|
276
|
+
return generateStartPrompt({
|
|
277
|
+
instructions: await readInstalledAsset('start'),
|
|
278
|
+
program,
|
|
279
|
+
request: userRequest,
|
|
280
|
+
root,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (stack.components.length === 0) {
|
|
284
|
+
return generateStartPrompt({
|
|
285
|
+
instructions: await readInstalledAsset('start'),
|
|
286
|
+
program,
|
|
287
|
+
request: userRequest,
|
|
288
|
+
root,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (['describe', 'program'].includes(task)) {
|
|
293
|
+
return generateExplanationPrompt({
|
|
294
|
+
instructions,
|
|
295
|
+
program,
|
|
296
|
+
request: userRequest,
|
|
297
|
+
root,
|
|
298
|
+
task,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const blueprint = await readBlueprint(root, { required: true, requireDescription: true });
|
|
303
|
+
stack ||= await readStack(root);
|
|
304
|
+
const [index, projectSkills] = await Promise.all([
|
|
305
|
+
buildProjectIndex({ projectRoot: root, write: false }),
|
|
306
|
+
inspectProjectSkills({ projectRoot: root, stack }),
|
|
307
|
+
]);
|
|
308
|
+
const missing = missingStackResources({ environment, resources: stack.resources });
|
|
309
|
+
const warnings = [
|
|
310
|
+
...(program.diagnostic ? [program.diagnostic] : []),
|
|
311
|
+
...missing,
|
|
312
|
+
...projectSkills.diagnostics,
|
|
313
|
+
...index.diagnostics,
|
|
314
|
+
];
|
|
315
|
+
const context = {
|
|
316
|
+
task,
|
|
317
|
+
projectRoot: root,
|
|
318
|
+
blueprint: { path: blueprint.path, description: blueprint.description },
|
|
319
|
+
stack: stackPromptContext(stack),
|
|
320
|
+
program: programContext(program),
|
|
321
|
+
resourceInputs: missing.length > 0
|
|
322
|
+
? { status: 'missing', diagnostics: missing }
|
|
323
|
+
: { status: 'present' },
|
|
324
|
+
verificationCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
|
|
325
|
+
agentSkills: { status: projectSkills.status },
|
|
326
|
+
codeIndex: codeIndexContext(index),
|
|
327
|
+
};
|
|
328
|
+
if (task === 'review') {
|
|
329
|
+
const verification = await inspectVerification({ projectRoot: root, stack });
|
|
330
|
+
context.verificationEvidence = { status: verification.status };
|
|
331
|
+
}
|
|
332
|
+
const prompt = renderPrompt({
|
|
333
|
+
instructions,
|
|
334
|
+
request: userRequest,
|
|
335
|
+
context,
|
|
336
|
+
guidance: stack.guidance,
|
|
337
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
338
|
+
cleanup: task === 'deslop' ? stack.deslop : '',
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
status: 'ready',
|
|
342
|
+
task,
|
|
343
|
+
prompt,
|
|
344
|
+
warnings,
|
|
345
|
+
verificationCommands: context.verificationCommands,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { GenesisError } from './errors.js';
|
|
6
|
+
import {
|
|
7
|
+
normalizeStackPieceId,
|
|
8
|
+
parseStackPieceSource,
|
|
9
|
+
} from './stack-piece.js';
|
|
10
|
+
|
|
11
|
+
const catalogRoot = fileURLToPath(new URL('../../stacks/pieces/', import.meta.url));
|
|
12
|
+
function builtinStackPiecePath(id) {
|
|
13
|
+
return path.join(catalogRoot, `${id}.md`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readBuiltinStackCatalog() {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = await readdir(catalogRoot, { withFileTypes: true });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
22
|
+
throw new GenesisError(
|
|
23
|
+
'STACK_CATALOG_INVALID',
|
|
24
|
+
'The installed Genesis package is missing its built-in stack catalog.',
|
|
25
|
+
{ catalogRoot },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const files = entries
|
|
32
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
33
|
+
.map((entry) => entry.name)
|
|
34
|
+
.sort();
|
|
35
|
+
const pieces = await Promise.all(files.map(async (file) => {
|
|
36
|
+
const id = normalizeStackPieceId(file.slice(0, -3));
|
|
37
|
+
const source = await readFile(builtinStackPiecePath(id), 'utf8');
|
|
38
|
+
return parseStackPieceSource(source, {
|
|
39
|
+
expectedId: id,
|
|
40
|
+
path: `builtin:${id}`,
|
|
41
|
+
});
|
|
42
|
+
}));
|
|
43
|
+
const catalog = new Map();
|
|
44
|
+
for (const piece of pieces) {
|
|
45
|
+
if (catalog.has(piece.id)) {
|
|
46
|
+
throw new GenesisError(
|
|
47
|
+
'STACK_CATALOG_INVALID',
|
|
48
|
+
`The built-in stack catalog contains duplicate piece ${piece.id}.`,
|
|
49
|
+
{ piece: piece.id },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
catalog.set(piece.id, piece);
|
|
53
|
+
}
|
|
54
|
+
return catalog;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function listBuiltinStackPieces() {
|
|
58
|
+
return [...(await readBuiltinStackCatalog()).values()]
|
|
59
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
60
|
+
.map((piece) => ({
|
|
61
|
+
id: piece.id,
|
|
62
|
+
description: piece.description,
|
|
63
|
+
guidance: piece.guidance,
|
|
64
|
+
requires: piece.requires,
|
|
65
|
+
conflicts: piece.conflicts,
|
|
66
|
+
indexers: piece.indexers,
|
|
67
|
+
launchTargets: piece.launchTargets.map(({ id }) => id),
|
|
68
|
+
workspaceSetup: piece.workspaceSetupSteps,
|
|
69
|
+
skill: piece.skill === null ? null : piece.skill.path,
|
|
70
|
+
resources: piece.resources.map(({ id }) => id),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
import { isSafeProcessExecutable, parseBacktickedArguments } from './stack-process.js';
|
|
3
|
+
|
|
4
|
+
function commandDiagnostic(stackPath, message, line, details = {}) {
|
|
5
|
+
throw new GenesisError(
|
|
6
|
+
'STACK_COMMANDS_INVALID',
|
|
7
|
+
message,
|
|
8
|
+
{
|
|
9
|
+
path: stackPath,
|
|
10
|
+
...(line === undefined ? {} : { line }),
|
|
11
|
+
...details,
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseStackCommandLines(lines, { path: stackPath } = {}) {
|
|
17
|
+
const commands = [];
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
20
|
+
const source = lines[index];
|
|
21
|
+
const line = index + 1;
|
|
22
|
+
if (!source.trim()) continue;
|
|
23
|
+
const entry = source.match(/^- Verify `([^`\r\n]+)`:[ \t]+(.+)$/u);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
commandDiagnostic(
|
|
26
|
+
stackPath,
|
|
27
|
+
'Every Commands entry must use `- Verify `label`: `command` `argument`...`.',
|
|
28
|
+
line,
|
|
29
|
+
{ observed: source },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tokenSource = entry[2].trim();
|
|
34
|
+
const argv = parseBacktickedArguments(tokenSource);
|
|
35
|
+
if (!argv) {
|
|
36
|
+
commandDiagnostic(
|
|
37
|
+
stackPath,
|
|
38
|
+
'Every command and argument must be one separate non-empty backticked value.',
|
|
39
|
+
line,
|
|
40
|
+
{ observed: source },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const command = argv[0];
|
|
45
|
+
if (
|
|
46
|
+
argv.some((value) => value.includes('\0'))
|
|
47
|
+
|| !isSafeProcessExecutable(command)
|
|
48
|
+
) {
|
|
49
|
+
commandDiagnostic(
|
|
50
|
+
stackPath,
|
|
51
|
+
'A stack command executable must be a PATH name or project-relative path without traversal.',
|
|
52
|
+
line,
|
|
53
|
+
{ command },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
commands.push({
|
|
58
|
+
label: entry[1],
|
|
59
|
+
argv,
|
|
60
|
+
line,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return commands;
|
|
65
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
function orderedClosure(catalog, selected, unknown, visiting, ordered) {
|
|
4
|
+
for (const id of selected) {
|
|
5
|
+
if (ordered.some((piece) => piece.id === id)) continue;
|
|
6
|
+
const piece = catalog.get(id);
|
|
7
|
+
if (!piece) {
|
|
8
|
+
unknown.add(id);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (visiting.has(id)) {
|
|
12
|
+
throw new GenesisError('STACK_DEPENDENCY_CYCLE', `Stack component dependency cycle includes ${id}.`);
|
|
13
|
+
}
|
|
14
|
+
visiting.add(id);
|
|
15
|
+
orderedClosure(catalog, piece.requires, unknown, visiting, ordered);
|
|
16
|
+
visiting.delete(id);
|
|
17
|
+
ordered.push(piece);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Adds dependency closure and rejects only real unknown or conflicting pieces. */
|
|
22
|
+
export function resolveStackPieces({ catalog, existing = [], requested = [] } = {}) {
|
|
23
|
+
const unknown = new Set();
|
|
24
|
+
const pieces = [];
|
|
25
|
+
orderedClosure(catalog, [...new Set([...existing, ...requested])], unknown, new Set(), pieces);
|
|
26
|
+
const installed = new Set(pieces.map(({ id }) => id));
|
|
27
|
+
for (const piece of pieces) {
|
|
28
|
+
const conflict = piece.conflicts.find((id) => installed.has(id));
|
|
29
|
+
if (conflict) {
|
|
30
|
+
throw new GenesisError(
|
|
31
|
+
'STACK_PIECE_CONFLICT',
|
|
32
|
+
`Stack components ${piece.id} and ${conflict} conflict.`,
|
|
33
|
+
{ component: piece.id, conflict },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { pieces, unknown: [...unknown].sort() };
|
|
38
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
import { isCanonicalProjectWorkdir } from './stack-process.js';
|
|
3
|
+
|
|
4
|
+
const DOTENV_LINE = /^- Dotenv `([^`\r\n]+)`[ \t]*$/u;
|
|
5
|
+
|
|
6
|
+
function invalid(environmentPath, message, line, details = {}) {
|
|
7
|
+
throw new GenesisError('STACK_ENVIRONMENT_FILES_INVALID', message, {
|
|
8
|
+
path: environmentPath,
|
|
9
|
+
...(line === undefined ? {} : { line }),
|
|
10
|
+
...details,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function environmentFilePath(value, environmentPath, line) {
|
|
15
|
+
if (value === '.' || !isCanonicalProjectWorkdir(value)) {
|
|
16
|
+
invalid(
|
|
17
|
+
environmentPath,
|
|
18
|
+
'Environment file paths must be canonical project-relative file paths.',
|
|
19
|
+
line,
|
|
20
|
+
{ observed: value },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseStackEnvironmentFileLines(lines, {
|
|
27
|
+
path: environmentPath = 'stack environment files',
|
|
28
|
+
} = {}) {
|
|
29
|
+
if (lines === undefined) return null;
|
|
30
|
+
const entries = lines.map((line) => line.trim()).filter(Boolean);
|
|
31
|
+
if (entries.length === 1 && entries[0] === '- Nothing.') return [];
|
|
32
|
+
const files = [];
|
|
33
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
34
|
+
const source = lines[index].trim();
|
|
35
|
+
if (!source) continue;
|
|
36
|
+
const entry = source.match(DOTENV_LINE);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
invalid(
|
|
39
|
+
environmentPath,
|
|
40
|
+
'Every Environment files entry must use `- Dotenv `project-relative-path``.',
|
|
41
|
+
index + 1,
|
|
42
|
+
{ observed: source },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
files.push({
|
|
46
|
+
format: 'dotenv',
|
|
47
|
+
path: environmentFilePath(entry[1], environmentPath, index + 1),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (files.length === 0) {
|
|
51
|
+
invalid(environmentPath, '## Environment files needs at least one Dotenv entry.');
|
|
52
|
+
}
|
|
53
|
+
const paths = files.map((file) => file.path);
|
|
54
|
+
if (new Set(paths).size !== paths.length) {
|
|
55
|
+
invalid(environmentPath, '## Environment files contains a duplicate path.');
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function composeStackEnvironmentFiles(components, projectFiles) {
|
|
61
|
+
if (projectFiles !== null) {
|
|
62
|
+
return projectFiles.map((file) => ({ ...file, source: 'project' }));
|
|
63
|
+
}
|
|
64
|
+
const files = new Map();
|
|
65
|
+
for (const component of components) {
|
|
66
|
+
for (const file of component.environmentFiles) {
|
|
67
|
+
const existing = files.get(file.path);
|
|
68
|
+
if (existing && existing.format !== file.format) {
|
|
69
|
+
invalid(
|
|
70
|
+
`component:${component.id}`,
|
|
71
|
+
`Stack components declare incompatible environment file formats for ${file.path}.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!existing) {
|
|
75
|
+
files.set(file.path, {
|
|
76
|
+
...file,
|
|
77
|
+
source: `component:${component.id}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
83
|
+
}
|