genesis-compiler 1.2.5 → 1.2.7
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 +45 -19
- package/docs/prompt-integration.md +1 -1
- package/docs/stack-components.md +19 -4
- package/package.json +2 -4
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/adopt.txt +6 -0
- package/src/cli.js +38 -10
- package/src/index/agent-skills.js +11 -3
- package/src/index/check.js +30 -2
- package/src/index/code-index.js +7 -2
- package/src/index/context.js +2 -2
- package/src/index/contracts.js +7 -0
- package/src/index/deployment.js +4 -2
- package/src/index/environment-files.js +8 -2
- package/src/index/init.js +2 -2
- package/src/index/launch.js +4 -1
- package/src/index/process.js +119 -5
- package/src/index/prompt.js +24 -16
- package/src/index/stack-catalog.js +111 -26
- package/src/index/stack.js +60 -8
- package/src/index/verification.js +15 -1
- package/src/index/workspace-setup.js +12 -2
- package/src/index.js +29 -17
- package/stacks/pieces/cpp.md +0 -34
- package/stacks/pieces/csharp.md +0 -22
- package/stacks/pieces/go.md +0 -22
- package/stacks/pieces/java.md +0 -22
- package/stacks/pieces/jskit-mysql.md +0 -56
- package/stacks/pieces/jskit-postgresql.md +0 -56
- package/stacks/pieces/jskit.md +0 -114
- package/stacks/pieces/kotlin.md +0 -22
- package/stacks/pieces/mysql.md +0 -29
- package/stacks/pieces/nodejs.md +0 -36
- package/stacks/pieces/php.md +0 -23
- package/stacks/pieces/postgresql.md +0 -30
- package/stacks/pieces/python.md +0 -23
- package/stacks/pieces/ruby.md +0 -22
- package/stacks/pieces/rust.md +0 -22
- package/stacks/pieces/shell.md +0 -23
- package/stacks/pieces/vue.md +0 -28
package/src/index/process.js
CHANGED
|
@@ -1,18 +1,120 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
3
|
import { GenesisError } from './errors.js';
|
|
4
4
|
|
|
5
5
|
const MAX_DIAGNOSTIC_OUTPUT = 16_384;
|
|
6
|
+
const DEFAULT_TERMINATION_GRACE_MS = 1_000;
|
|
6
7
|
|
|
7
|
-
function
|
|
8
|
+
function processTerminationError(code, message) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.code = code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function signalProcessTree(child, signal) {
|
|
15
|
+
if (!Number.isSafeInteger(child?.pid) || child.pid < 1) return;
|
|
16
|
+
try {
|
|
17
|
+
if (process.platform === 'win32') child.kill(signal);
|
|
18
|
+
else process.kill(-child.pid, signal);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error?.code !== 'ESRCH') throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function executeFile(command, args, {
|
|
25
|
+
maxBuffer,
|
|
26
|
+
signal,
|
|
27
|
+
terminationGraceMs = DEFAULT_TERMINATION_GRACE_MS,
|
|
28
|
+
timeoutMs,
|
|
29
|
+
...options
|
|
30
|
+
}) {
|
|
8
31
|
return new Promise((resolve, reject) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
32
|
+
const stdoutChunks = [];
|
|
33
|
+
const stderrChunks = [];
|
|
34
|
+
let stdoutBytes = 0;
|
|
35
|
+
let stderrBytes = 0;
|
|
36
|
+
let forcedError = null;
|
|
37
|
+
let killTimer = null;
|
|
38
|
+
let spawnError = null;
|
|
39
|
+
let timeoutTimer = null;
|
|
40
|
+
let settled = false;
|
|
41
|
+
const cleanup = () => {
|
|
42
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
43
|
+
if (killTimer) clearTimeout(killTimer);
|
|
44
|
+
signal?.removeEventListener('abort', abort);
|
|
45
|
+
};
|
|
46
|
+
const child = spawn(command, args, {
|
|
47
|
+
...options,
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
});
|
|
50
|
+
const terminate = (error) => {
|
|
51
|
+
if (settled || forcedError) return;
|
|
52
|
+
forcedError = error;
|
|
53
|
+
signalProcessTree(child, 'SIGTERM');
|
|
54
|
+
killTimer = setTimeout(() => {
|
|
55
|
+
if (!settled) signalProcessTree(child, 'SIGKILL');
|
|
56
|
+
}, terminationGraceMs);
|
|
57
|
+
killTimer.unref?.();
|
|
58
|
+
};
|
|
59
|
+
const collect = (chunks, chunk, stream) => {
|
|
60
|
+
chunks.push(chunk);
|
|
61
|
+
if (stream === 'stdout') stdoutBytes += chunk.length;
|
|
62
|
+
else stderrBytes += chunk.length;
|
|
63
|
+
if (Number.isFinite(maxBuffer) && Math.max(stdoutBytes, stderrBytes) > maxBuffer) {
|
|
64
|
+
terminate(processTerminationError(
|
|
65
|
+
'GENESIS_PROCESS_OUTPUT_LIMIT',
|
|
66
|
+
`${command} exceeded its diagnostic output limit.`,
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
child.stdout.on('data', (chunk) => collect(stdoutChunks, chunk, 'stdout'));
|
|
71
|
+
child.stderr.on('data', (chunk) => collect(stderrChunks, chunk, 'stderr'));
|
|
72
|
+
child.once('error', (error) => {
|
|
73
|
+
spawnError = error;
|
|
74
|
+
});
|
|
75
|
+
child.once('close', (status, processSignal) => {
|
|
76
|
+
settled = true;
|
|
77
|
+
cleanup();
|
|
78
|
+
const stdout = Buffer.concat(stdoutChunks);
|
|
79
|
+
const stderr = Buffer.concat(stderrChunks);
|
|
80
|
+
if (forcedError) {
|
|
81
|
+
forcedError.stdout = stdout;
|
|
82
|
+
forcedError.stderr = stderr;
|
|
83
|
+
forcedError.signal = processSignal || null;
|
|
84
|
+
reject(forcedError);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (spawnError) {
|
|
88
|
+
spawnError.stdout = stdout;
|
|
89
|
+
spawnError.stderr = stderr;
|
|
90
|
+
spawnError.signal = processSignal || null;
|
|
91
|
+
reject(spawnError);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (status !== 0) {
|
|
95
|
+
const error = new Error(`${command} exited with status ${status ?? 'unknown'}.`);
|
|
96
|
+
error.code = status;
|
|
97
|
+
error.signal = processSignal || null;
|
|
98
|
+
error.stdout = stdout;
|
|
99
|
+
error.stderr = stderr;
|
|
11
100
|
reject(error);
|
|
12
101
|
return;
|
|
13
102
|
}
|
|
14
103
|
resolve({ stdout, stderr });
|
|
15
104
|
});
|
|
105
|
+
const abort = () => terminate(processTerminationError(
|
|
106
|
+
'GENESIS_PROCESS_ABORTED',
|
|
107
|
+
`${command} was cancelled.`,
|
|
108
|
+
));
|
|
109
|
+
if (signal?.aborted) abort();
|
|
110
|
+
else signal?.addEventListener('abort', abort, { once: true });
|
|
111
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
112
|
+
timeoutTimer = setTimeout(() => terminate(processTerminationError(
|
|
113
|
+
'GENESIS_PROCESS_TIMEOUT',
|
|
114
|
+
`${command} timed out after ${timeoutMs} ms.`,
|
|
115
|
+
)), timeoutMs);
|
|
116
|
+
timeoutTimer.unref?.();
|
|
117
|
+
}
|
|
16
118
|
child.stdin?.end();
|
|
17
119
|
});
|
|
18
120
|
}
|
|
@@ -39,21 +141,33 @@ export async function runProcess(command, args, {
|
|
|
39
141
|
env = process.env,
|
|
40
142
|
maxBytes = 32 * 1024 * 1024,
|
|
41
143
|
code = 'PROCESS_EXEC_FAILED',
|
|
144
|
+
signal,
|
|
145
|
+
terminationGraceMs,
|
|
146
|
+
timeoutMs,
|
|
42
147
|
} = {}) {
|
|
43
148
|
try {
|
|
44
149
|
const { stdout, stderr } = await executeFile(command, args, {
|
|
45
150
|
cwd,
|
|
151
|
+
detached: process.platform !== 'win32',
|
|
46
152
|
env,
|
|
47
153
|
encoding: 'buffer',
|
|
48
154
|
maxBuffer: maxBytes,
|
|
49
155
|
shell: false,
|
|
156
|
+
signal,
|
|
157
|
+
terminationGraceMs,
|
|
158
|
+
timeoutMs,
|
|
50
159
|
windowsHide: true,
|
|
51
160
|
});
|
|
52
161
|
return { status: 0, signal: null, stdout, stderr };
|
|
53
162
|
} catch (error) {
|
|
54
163
|
const stdout = Buffer.isBuffer(error.stdout) ? error.stdout : Buffer.from(error.stdout || '');
|
|
55
164
|
const stderr = Buffer.isBuffer(error.stderr) ? error.stderr : Buffer.from(error.stderr || '');
|
|
56
|
-
|
|
165
|
+
const diagnosticCode = error.code === 'GENESIS_PROCESS_ABORTED'
|
|
166
|
+
? 'PROCESS_ABORTED'
|
|
167
|
+
: error.code === 'GENESIS_PROCESS_TIMEOUT'
|
|
168
|
+
? 'PROCESS_TIMEOUT'
|
|
169
|
+
: code;
|
|
170
|
+
throw new GenesisError(diagnosticCode, `${command} failed: ${error.message}`, {
|
|
57
171
|
command,
|
|
58
172
|
args,
|
|
59
173
|
status: typeof error.code === 'number' ? error.code : null,
|
package/src/index/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js
|
|
|
3
3
|
import { buildProjectIndex, MACHINE_CITY_PATH, PROGRAM_CITY_PATH } from './code-index.js';
|
|
4
4
|
import { BLUEPRINT_SKELETON_SOURCE, readBlueprint } from './blueprint.js';
|
|
5
5
|
import { readStack, stackPromptContext } from './stack.js';
|
|
6
|
-
import {
|
|
6
|
+
import { listStackCatalogPieces } from './stack-catalog.js';
|
|
7
7
|
import { GenesisError } from './errors.js';
|
|
8
8
|
import { gitContext } from './git.js';
|
|
9
9
|
import { inspectProgram } from './program.js';
|
|
@@ -120,12 +120,13 @@ function renderPrompt({
|
|
|
120
120
|
].join('\n');
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
async function generateAdoptionPrompt({ environment, instructions, program, request, root }) {
|
|
123
|
+
async function generateAdoptionPrompt({ environment, instructions, program, request, root, stackPackages }) {
|
|
124
124
|
const blueprint = await readBlueprint(root, { required: true });
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
const stack = await readStack(root, { stackPackages });
|
|
126
|
+
const availableStackPackages = [...new Set([...stack.stackPackages, ...stackPackages])];
|
|
127
|
+
const [index, catalog] = await Promise.all([
|
|
128
|
+
buildProjectIndex({ projectRoot: root, stackPackages, write: false }),
|
|
129
|
+
listStackCatalogPieces({ projectRoot: root, stackPackages: availableStackPackages }),
|
|
129
130
|
]);
|
|
130
131
|
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
131
132
|
const missing = missingStackResources({
|
|
@@ -165,14 +166,14 @@ async function generateAdoptionPrompt({ environment, instructions, program, requ
|
|
|
165
166
|
};
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
async function generateExplanationPrompt({ instructions, program, request, root, task }) {
|
|
169
|
+
async function generateExplanationPrompt({ instructions, program, request, root, stackPackages, task }) {
|
|
169
170
|
const blueprint = await readBlueprint(root, {
|
|
170
171
|
required: true,
|
|
171
172
|
requireDescription: task === 'program',
|
|
172
173
|
});
|
|
173
174
|
const [stack, index] = await Promise.all([
|
|
174
|
-
readStack(root),
|
|
175
|
-
buildProjectIndex({ projectRoot: root, write: false }),
|
|
175
|
+
readStack(root, { stackPackages }),
|
|
176
|
+
buildProjectIndex({ projectRoot: root, stackPackages, write: false }),
|
|
176
177
|
]);
|
|
177
178
|
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
178
179
|
const blueprintContext = task === 'describe'
|
|
@@ -204,7 +205,7 @@ async function generateExplanationPrompt({ instructions, program, request, root,
|
|
|
204
205
|
};
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
async function generateStartPrompt({ instructions, program, request, root }) {
|
|
208
|
+
async function generateStartPrompt({ instructions, program, request, root, stackPackages }) {
|
|
208
209
|
let blueprint;
|
|
209
210
|
try {
|
|
210
211
|
blueprint = await readBlueprint(root, { required: true });
|
|
@@ -231,10 +232,11 @@ async function generateStartPrompt({ instructions, program, request, root }) {
|
|
|
231
232
|
verificationCommands: [],
|
|
232
233
|
};
|
|
233
234
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
const stack = await readStack(root, { stackPackages });
|
|
236
|
+
const availableStackPackages = [...new Set([...stack.stackPackages, ...stackPackages])];
|
|
237
|
+
const [index, catalog] = await Promise.all([
|
|
238
|
+
buildProjectIndex({ projectRoot: root, stackPackages, write: false }),
|
|
239
|
+
listStackCatalogPieces({ projectRoot: root, stackPackages: availableStackPackages }),
|
|
238
240
|
]);
|
|
239
241
|
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
240
242
|
const newProject = stack.components.length === 0 && index.fileCount === 0;
|
|
@@ -273,6 +275,7 @@ export async function generateProjectPrompt({
|
|
|
273
275
|
environment = process.env,
|
|
274
276
|
projectRoot,
|
|
275
277
|
request = '',
|
|
278
|
+
stackPackages = [],
|
|
276
279
|
task = 'work',
|
|
277
280
|
} = {}) {
|
|
278
281
|
if (!TASKS.has(task)) {
|
|
@@ -314,6 +317,7 @@ export async function generateProjectPrompt({
|
|
|
314
317
|
program,
|
|
315
318
|
request: userRequest,
|
|
316
319
|
root,
|
|
320
|
+
stackPackages,
|
|
317
321
|
});
|
|
318
322
|
}
|
|
319
323
|
if (task === 'adopt') {
|
|
@@ -323,12 +327,13 @@ export async function generateProjectPrompt({
|
|
|
323
327
|
program,
|
|
324
328
|
request: userRequest,
|
|
325
329
|
root,
|
|
330
|
+
stackPackages,
|
|
326
331
|
});
|
|
327
332
|
}
|
|
328
333
|
let stack;
|
|
329
334
|
if (task === 'work') {
|
|
330
335
|
try {
|
|
331
|
-
stack = await readStack(root);
|
|
336
|
+
stack = await readStack(root, { stackPackages });
|
|
332
337
|
} catch (error) {
|
|
333
338
|
if (error?.code !== 'STACK_REQUIRED') throw error;
|
|
334
339
|
return generateStartPrompt({
|
|
@@ -336,6 +341,7 @@ export async function generateProjectPrompt({
|
|
|
336
341
|
program,
|
|
337
342
|
request: userRequest,
|
|
338
343
|
root,
|
|
344
|
+
stackPackages,
|
|
339
345
|
});
|
|
340
346
|
}
|
|
341
347
|
if (stack.components.length === 0) {
|
|
@@ -344,6 +350,7 @@ export async function generateProjectPrompt({
|
|
|
344
350
|
program,
|
|
345
351
|
request: userRequest,
|
|
346
352
|
root,
|
|
353
|
+
stackPackages,
|
|
347
354
|
});
|
|
348
355
|
}
|
|
349
356
|
}
|
|
@@ -353,12 +360,13 @@ export async function generateProjectPrompt({
|
|
|
353
360
|
program,
|
|
354
361
|
request: userRequest,
|
|
355
362
|
root,
|
|
363
|
+
stackPackages,
|
|
356
364
|
task,
|
|
357
365
|
});
|
|
358
366
|
}
|
|
359
367
|
|
|
360
368
|
const blueprint = await readBlueprint(root, { required: true, requireDescription: true });
|
|
361
|
-
stack ||= await readStack(root);
|
|
369
|
+
stack ||= await readStack(root, { stackPackages });
|
|
362
370
|
const [index, projectSkills] = await Promise.all([
|
|
363
371
|
buildProjectIndex({ projectRoot: root, write: false }),
|
|
364
372
|
inspectProjectSkills({ projectRoot: root, stack }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import { GenesisError } from './errors.js';
|
|
6
6
|
import {
|
|
@@ -8,54 +8,138 @@ import {
|
|
|
8
8
|
parseStackPieceSource,
|
|
9
9
|
} from './stack-piece.js';
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/u;
|
|
13
|
+
|
|
14
|
+
export function normalizeStackPackageName(value) {
|
|
15
|
+
const name = String(value ?? '').trim();
|
|
16
|
+
if (!PACKAGE_NAME.test(name)) {
|
|
17
|
+
throw new GenesisError(
|
|
18
|
+
'STACK_PACKAGE_INVALID',
|
|
19
|
+
'Stack package names must be ordinary npm package names.',
|
|
20
|
+
{ package: value },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function packageDirectory(packageName, projectRoot) {
|
|
27
|
+
const roots = [
|
|
28
|
+
...(projectRoot ? createRequire(path.join(projectRoot, 'package.json')).resolve.paths(packageName) || [] : []),
|
|
29
|
+
...(require.resolve.paths(packageName) || []),
|
|
30
|
+
];
|
|
31
|
+
for (const modulesRoot of [...new Set(roots)]) {
|
|
32
|
+
const manifest = path.join(modulesRoot, packageName, 'package.json');
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(await readFile(manifest, 'utf8'));
|
|
35
|
+
if (value?.name === packageName) return { directory: path.dirname(manifest), manifest: value };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) {
|
|
38
|
+
throw new GenesisError(
|
|
39
|
+
'STACK_PACKAGE_INVALID',
|
|
40
|
+
`Cannot read Stack package ${packageName}: ${error.message}.`,
|
|
41
|
+
{ package: packageName },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new GenesisError(
|
|
47
|
+
'STACK_PACKAGE_UNAVAILABLE',
|
|
48
|
+
`Selected Stack package is unavailable: ${packageName}. Install it beside Genesis or in the project.`,
|
|
49
|
+
{ package: packageName },
|
|
50
|
+
);
|
|
14
51
|
}
|
|
15
52
|
|
|
16
|
-
|
|
53
|
+
async function readPieceDirectory(
|
|
54
|
+
directory,
|
|
55
|
+
sourcePrefix,
|
|
56
|
+
stackPackage = null,
|
|
57
|
+
stackPackageRoot = null,
|
|
58
|
+
) {
|
|
17
59
|
let entries;
|
|
18
60
|
try {
|
|
19
|
-
entries = await readdir(
|
|
61
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
20
62
|
} catch (error) {
|
|
21
|
-
if (
|
|
63
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
|
|
22
64
|
throw new GenesisError(
|
|
23
65
|
'STACK_CATALOG_INVALID',
|
|
24
|
-
|
|
25
|
-
{
|
|
66
|
+
`Stack catalog is missing its pieces directory: ${sourcePrefix}.`,
|
|
67
|
+
{ directory, package: stackPackage },
|
|
26
68
|
);
|
|
27
69
|
}
|
|
28
70
|
throw error;
|
|
29
71
|
}
|
|
30
|
-
|
|
31
72
|
const files = entries
|
|
32
73
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
33
74
|
.map((entry) => entry.name)
|
|
34
75
|
.sort();
|
|
35
|
-
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
throw new GenesisError(
|
|
78
|
+
'STACK_CATALOG_INVALID',
|
|
79
|
+
`Stack catalog contains no pieces: ${sourcePrefix}.`,
|
|
80
|
+
{ directory, package: stackPackage },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return Promise.all(files.map(async (file) => {
|
|
36
84
|
const id = normalizeStackPieceId(file.slice(0, -3));
|
|
37
|
-
const source = await readFile(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
const source = await readFile(path.join(directory, file), 'utf8');
|
|
86
|
+
const piece = parseStackPieceSource(source, { expectedId: id, path: `${sourcePrefix}:${id}` });
|
|
87
|
+
return {
|
|
88
|
+
...piece,
|
|
89
|
+
skill: piece.skill === null ? null : { ...piece.skill, resolveFrom: stackPackageRoot },
|
|
90
|
+
stackPackage,
|
|
91
|
+
};
|
|
42
92
|
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readExternalStackPieces(packageName, projectRoot) {
|
|
96
|
+
const resolved = await packageDirectory(packageName, projectRoot);
|
|
97
|
+
const relative = resolved.manifest?.genesis?.stackPieces;
|
|
98
|
+
if (
|
|
99
|
+
typeof relative !== 'string'
|
|
100
|
+
|| !relative
|
|
101
|
+
|| relative.includes('\\')
|
|
102
|
+
|| path.isAbsolute(relative)
|
|
103
|
+
|| relative.split('/').some((part) => !part || ['.', '..'].includes(part))
|
|
104
|
+
) {
|
|
105
|
+
throw new GenesisError(
|
|
106
|
+
'STACK_PACKAGE_INVALID',
|
|
107
|
+
`Stack package ${packageName} must declare one safe genesis.stackPieces directory.`,
|
|
108
|
+
{ package: packageName },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return readPieceDirectory(
|
|
112
|
+
path.join(resolved.directory, relative),
|
|
113
|
+
`npm:${packageName}`,
|
|
114
|
+
packageName,
|
|
115
|
+
resolved.directory,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function readStackCatalog({ projectRoot, stackPackages = [] } = {}) {
|
|
43
120
|
const catalog = new Map();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
121
|
+
const packages = [...new Set(stackPackages.map(normalizeStackPackageName))].sort();
|
|
122
|
+
for (const packageName of packages) {
|
|
123
|
+
for (const piece of await readExternalStackPieces(packageName, projectRoot)) {
|
|
124
|
+
const previous = catalog.get(piece.id);
|
|
125
|
+
if (previous) {
|
|
126
|
+
throw new GenesisError(
|
|
127
|
+
'STACK_CATALOG_COLLISION',
|
|
128
|
+
`Stack package ${packageName} collides with installed piece ${piece.id}.`,
|
|
129
|
+
{
|
|
130
|
+
piece: piece.id,
|
|
131
|
+
packages: [previous.stackPackage || 'genesis-compiler', packageName],
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
catalog.set(piece.id, piece);
|
|
51
136
|
}
|
|
52
|
-
catalog.set(piece.id, piece);
|
|
53
137
|
}
|
|
54
138
|
return catalog;
|
|
55
139
|
}
|
|
56
140
|
|
|
57
|
-
export async function
|
|
58
|
-
return [...(await
|
|
141
|
+
export async function listStackCatalogPieces(options = {}) {
|
|
142
|
+
return [...(await readStackCatalog(options)).values()]
|
|
59
143
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
60
144
|
.map((piece) => ({
|
|
61
145
|
id: piece.id,
|
|
@@ -71,6 +155,7 @@ export async function listBuiltinStackPieces() {
|
|
|
71
155
|
launchTargets: piece.launchTargets.map(({ id }) => id),
|
|
72
156
|
workspaceSetup: piece.workspaceSetupSteps,
|
|
73
157
|
skill: piece.skill === null ? null : piece.skill.path,
|
|
158
|
+
stackPackage: piece.stackPackage,
|
|
74
159
|
resources: piece.resources.map(({ id }) => id),
|
|
75
160
|
}));
|
|
76
161
|
}
|
package/src/index/stack.js
CHANGED
|
@@ -3,7 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { syncProjectSkills } from './agent-skills.js';
|
|
5
5
|
import { GenesisError } from './errors.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
normalizeStackPackageName,
|
|
8
|
+
readStackCatalog,
|
|
9
|
+
} from './stack-catalog.js';
|
|
7
10
|
import { parseStackCommandLines } from './stack-command.js';
|
|
8
11
|
import { composeStackCityPresentation } from './stack-city-presentation.js';
|
|
9
12
|
import { composeStackDeployment, parseStackDeploymentLines } from './stack-deployment.js';
|
|
@@ -31,8 +34,10 @@ import { STACK_PATH } from './paths.js';
|
|
|
31
34
|
import { sha256, stableJson, writeFileAtomic } from './utils.js';
|
|
32
35
|
|
|
33
36
|
const COMPONENT_LINE = /^- `([a-z0-9]+(?:-[a-z0-9]+)*)`$/u;
|
|
37
|
+
const STACK_PACKAGE_LINE = /^- `((?:@[a-z0-9._-]+\/)?[a-z0-9._-]+)`$/u;
|
|
34
38
|
export const EMPTY_STACK_SOURCE = '# Stack\n\n## Components\n';
|
|
35
39
|
const STACK_SECTIONS = new Set([
|
|
40
|
+
'Stack packages',
|
|
36
41
|
'Components',
|
|
37
42
|
'Resources',
|
|
38
43
|
'Environment defaults',
|
|
@@ -86,6 +91,27 @@ function componentIds(sections) {
|
|
|
86
91
|
return ids;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
function stackPackageNames(sections) {
|
|
95
|
+
const lines = (sections.get('Stack packages') || []).filter((line) => line.trim());
|
|
96
|
+
const names = lines.map((line) => {
|
|
97
|
+
const match = line.trim().match(STACK_PACKAGE_LINE);
|
|
98
|
+
if (!match) {
|
|
99
|
+
throw new GenesisError(
|
|
100
|
+
'STACK_INVALID',
|
|
101
|
+
'Each Stack package must be exactly one bullet containing its backticked npm package name.',
|
|
102
|
+
{ path: STACK_PATH, observed: line },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return normalizeStackPackageName(match[1]);
|
|
106
|
+
});
|
|
107
|
+
if (new Set(names).size !== names.length) {
|
|
108
|
+
throw new GenesisError('STACK_INVALID', 'Stack contains a duplicate Stack package.', {
|
|
109
|
+
path: STACK_PATH,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return names;
|
|
113
|
+
}
|
|
114
|
+
|
|
89
115
|
function knownPieces(resolution) {
|
|
90
116
|
if (resolution.unknown.length > 0) {
|
|
91
117
|
throw new GenesisError(
|
|
@@ -112,10 +138,14 @@ function renderStack({
|
|
|
112
138
|
environmentFileLines = null,
|
|
113
139
|
launchLines = null,
|
|
114
140
|
resourceLines = null,
|
|
141
|
+
stackPackages = [],
|
|
115
142
|
workspaceSetupLines = null,
|
|
116
143
|
}) {
|
|
117
144
|
return [
|
|
118
145
|
'# Stack',
|
|
146
|
+
...(stackPackages.length > 0
|
|
147
|
+
? ['', '## Stack packages', ...stackPackages.map((name) => `- \`${name}\``)]
|
|
148
|
+
: []),
|
|
119
149
|
'',
|
|
120
150
|
'## Components',
|
|
121
151
|
...componentIdsValue.map((id) => `- \`${id}\``),
|
|
@@ -140,7 +170,7 @@ function renderStack({
|
|
|
140
170
|
].join('\n');
|
|
141
171
|
}
|
|
142
172
|
|
|
143
|
-
export async function addStackPieces({ pieces, projectRoot }) {
|
|
173
|
+
export async function addStackPieces({ pieces, projectRoot, stackPackages = [] }) {
|
|
144
174
|
if (!Array.isArray(pieces) || pieces.length === 0) {
|
|
145
175
|
throw new GenesisError('STACK_PIECE_REQUIRED', 'genesis stack add requires at least one component.');
|
|
146
176
|
}
|
|
@@ -158,11 +188,21 @@ export async function addStackPieces({ pieces, projectRoot }) {
|
|
|
158
188
|
}
|
|
159
189
|
const sections = parseSections(source);
|
|
160
190
|
const existing = componentIds(sections);
|
|
161
|
-
const
|
|
162
|
-
const
|
|
191
|
+
const declaredPackages = stackPackageNames(sections);
|
|
192
|
+
const availablePackages = [...new Set([
|
|
193
|
+
...declaredPackages,
|
|
194
|
+
...stackPackages.map(normalizeStackPackageName),
|
|
195
|
+
])].sort();
|
|
196
|
+
const catalog = await readStackCatalog({ projectRoot, stackPackages: availablePackages });
|
|
197
|
+
const selectedPieces = await Promise.all(
|
|
163
198
|
knownPieces(resolveStackPieces({ catalog, existing, requested }))
|
|
164
199
|
.map((piece) => customizePiece(projectRoot, piece)),
|
|
165
|
-
)
|
|
200
|
+
);
|
|
201
|
+
const selected = selectedPieces.map(({ id }) => id);
|
|
202
|
+
const selectedPackages = [...new Set([
|
|
203
|
+
...declaredPackages,
|
|
204
|
+
...selectedPieces.map(({ stackPackage }) => stackPackage).filter(Boolean),
|
|
205
|
+
])].sort();
|
|
166
206
|
const commandLines = parseStackCommandLines(sections.get('Commands') || [], { path: STACK_PATH })
|
|
167
207
|
.map(({ label, argv }) => `- Verify \`${label}\`: ${argv.map((value) => `\`${value}\``).join(' ')}`);
|
|
168
208
|
const resourceLines = sections.has('Resources') ? sections.get('Resources') : null;
|
|
@@ -203,11 +243,15 @@ export async function addStackPieces({ pieces, projectRoot }) {
|
|
|
203
243
|
environmentFileLines,
|
|
204
244
|
launchLines,
|
|
205
245
|
resourceLines,
|
|
246
|
+
stackPackages: selectedPackages,
|
|
206
247
|
workspaceSetupLines,
|
|
207
248
|
});
|
|
208
249
|
const stackChanged = rendered !== source;
|
|
209
250
|
if (stackChanged) await writeFileAtomic(location, rendered);
|
|
210
|
-
const skills = await syncProjectSkills({
|
|
251
|
+
const skills = await syncProjectSkills({
|
|
252
|
+
projectRoot,
|
|
253
|
+
stack: await readStack(projectRoot, { stackPackages: availablePackages }),
|
|
254
|
+
});
|
|
211
255
|
const changedFiles = [
|
|
212
256
|
...(stackChanged ? [STACK_PATH] : []),
|
|
213
257
|
...skills.changedFiles,
|
|
@@ -262,7 +306,7 @@ async function customizePiece(projectRoot, piece) {
|
|
|
262
306
|
}));
|
|
263
307
|
}
|
|
264
308
|
|
|
265
|
-
export async function readStack(projectRoot) {
|
|
309
|
+
export async function readStack(projectRoot, { stackPackages = [] } = {}) {
|
|
266
310
|
const location = path.join(projectRoot, STACK_PATH);
|
|
267
311
|
let source;
|
|
268
312
|
try {
|
|
@@ -277,7 +321,12 @@ export async function readStack(projectRoot) {
|
|
|
277
321
|
throw error;
|
|
278
322
|
}
|
|
279
323
|
const sections = parseSections(source);
|
|
280
|
-
const
|
|
324
|
+
const declaredPackages = stackPackageNames(sections);
|
|
325
|
+
const availablePackages = [...new Set([
|
|
326
|
+
...declaredPackages,
|
|
327
|
+
...stackPackages.map(normalizeStackPackageName),
|
|
328
|
+
])].sort();
|
|
329
|
+
const catalog = await readStackCatalog({ projectRoot, stackPackages: availablePackages });
|
|
281
330
|
const components = await Promise.all(knownPieces(resolveStackPieces({
|
|
282
331
|
catalog,
|
|
283
332
|
requested: componentIds(sections),
|
|
@@ -325,6 +374,7 @@ export async function readStack(projectRoot) {
|
|
|
325
374
|
return {
|
|
326
375
|
path: STACK_PATH,
|
|
327
376
|
identityHash: sha256(stableJson({
|
|
377
|
+
stackPackages: declaredPackages,
|
|
328
378
|
components: components.map(({ id }) => id),
|
|
329
379
|
cityExclusions: cityPresentation.exclusions,
|
|
330
380
|
cityRegions: cityPresentation.regions,
|
|
@@ -337,6 +387,7 @@ export async function readStack(projectRoot) {
|
|
|
337
387
|
workspaceSetup,
|
|
338
388
|
})),
|
|
339
389
|
components,
|
|
390
|
+
stackPackages: declaredPackages,
|
|
340
391
|
cityExclusions: cityPresentation.exclusions,
|
|
341
392
|
cityRegions: cityPresentation.regions,
|
|
342
393
|
commands,
|
|
@@ -354,6 +405,7 @@ export async function readStack(projectRoot) {
|
|
|
354
405
|
|
|
355
406
|
export function stackPromptContext(stack) {
|
|
356
407
|
return {
|
|
408
|
+
stackPackages: stack.stackPackages,
|
|
357
409
|
components: stack.components.map((piece) => ({
|
|
358
410
|
id: piece.id,
|
|
359
411
|
description: piece.description,
|