genesis-compiler 1.2.1 → 1.2.3
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 +52 -17
- package/docs/assurance-model.md +15 -0
- package/docs/stack-components.md +113 -14
- package/package.json +2 -2
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/start.txt +5 -0
- package/skills/genesis-project/SKILL.md +6 -3
- package/src/cli.js +24 -1
- package/src/index/agent-skills.js +25 -4
- package/src/index/check.js +5 -1
- package/src/index/city-presentation.js +83 -0
- package/src/index/code-index.js +24 -3
- package/src/index/deployment.js +31 -0
- package/src/index/environment-files.js +10 -3
- package/src/index/init.js +1 -1
- package/src/index/launch.js +6 -1
- package/src/index/prompt.js +5 -1
- package/src/index/stack-catalog.js +3 -0
- package/src/index/stack-city-presentation.js +178 -0
- package/src/index/stack-deployment.js +204 -0
- package/src/index/stack-environment-defaults.js +79 -0
- package/src/index/stack-launch.js +37 -0
- package/src/index/stack-piece.js +19 -0
- package/src/index/stack-workspace-setup.js +7 -3
- package/src/index/stack.js +35 -0
- package/src/index/utils.js +1 -1
- package/src/index/verification.js +31 -1
- package/src/index/workspace-setup.js +101 -10
- package/src/index.js +13 -1
- package/stacks/pieces/jskit-mysql.md +5 -1
- package/stacks/pieces/jskit-postgresql.md +5 -1
- package/stacks/pieces/jskit.md +30 -9
|
@@ -118,6 +118,7 @@ function targetDraft(match, launchPath, line) {
|
|
|
118
118
|
workdir: '.',
|
|
119
119
|
preferredPort: null,
|
|
120
120
|
urlPath: '/',
|
|
121
|
+
readiness: null,
|
|
121
122
|
runtimeRequirements: [],
|
|
122
123
|
steps: [],
|
|
123
124
|
previewIdentity: null,
|
|
@@ -126,6 +127,27 @@ function targetDraft(match, launchPath, line) {
|
|
|
126
127
|
};
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
function parseReadiness(source, launchPath, line) {
|
|
131
|
+
const match = source.match(/^- Ready when: `GET` `([^`\r\n]+)` returns `([0-9]{3})`$/u);
|
|
132
|
+
if (!match) {
|
|
133
|
+
invalid(
|
|
134
|
+
launchPath,
|
|
135
|
+
'Launch target readiness must use `- Ready when: `GET` `/path` returns `200``.',
|
|
136
|
+
line,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const status = Number(match[2]);
|
|
140
|
+
if (!Number.isInteger(status) || status < 200 || status > 399) {
|
|
141
|
+
invalid(launchPath, 'Launch target readiness status must be from 200 through 399.', line);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
kind: 'http',
|
|
145
|
+
method: 'GET',
|
|
146
|
+
path: parseUrlPath(match[1], launchPath, line),
|
|
147
|
+
status,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
129
151
|
function parsePreferredPort(value, launchPath, line) {
|
|
130
152
|
if (!/^[0-9]+$/u.test(value)) {
|
|
131
153
|
invalid(launchPath, 'Launch target preferred port must be an integer from 1024 through 65535.', line);
|
|
@@ -204,6 +226,17 @@ function parseTargetLine(draft, source, launchPath, line) {
|
|
|
204
226
|
);
|
|
205
227
|
return;
|
|
206
228
|
}
|
|
229
|
+
if (source.startsWith('- Ready when: ')) {
|
|
230
|
+
setOnce(
|
|
231
|
+
draft,
|
|
232
|
+
'readiness',
|
|
233
|
+
parseReadiness(source, launchPath, line),
|
|
234
|
+
launchPath,
|
|
235
|
+
line,
|
|
236
|
+
'Launch target Ready when entry',
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
207
240
|
const step = source.match(/^- (Prepare|Serve) `([^`\r\n]+)`:[ \t]+(.+)$/u);
|
|
208
241
|
if (step) {
|
|
209
242
|
draft.steps.push({
|
|
@@ -336,6 +369,9 @@ function normalizeTarget(draft, launchPath) {
|
|
|
336
369
|
if (serverSteps.length !== 1 || draft.steps.at(-1).role !== 'server') {
|
|
337
370
|
invalid(launchPath, `Launch target ${draft.id} needs exactly one final Serve step.`, draft.line);
|
|
338
371
|
}
|
|
372
|
+
if (!draft.readiness) {
|
|
373
|
+
invalid(launchPath, `Launch target ${draft.id} needs one Ready when entry.`, draft.line);
|
|
374
|
+
}
|
|
339
375
|
return {
|
|
340
376
|
id: draft.id,
|
|
341
377
|
label: draft.label,
|
|
@@ -343,6 +379,7 @@ function normalizeTarget(draft, launchPath) {
|
|
|
343
379
|
workdir: draft.workdir,
|
|
344
380
|
preferredPort: draft.preferredPort,
|
|
345
381
|
urlPath: draft.urlPath,
|
|
382
|
+
readiness: draft.readiness,
|
|
346
383
|
runtimeRequirements: draft.runtimeRequirements,
|
|
347
384
|
steps: draft.steps,
|
|
348
385
|
...(draft.previewIdentity
|
package/src/index/stack-piece.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { GenesisError } from './errors.js';
|
|
2
2
|
import { parseStackCommandLines } from './stack-command.js';
|
|
3
|
+
import { parseStackCityPresentationLines } from './stack-city-presentation.js';
|
|
4
|
+
import { parseStackDeploymentLines } from './stack-deployment.js';
|
|
5
|
+
import { parseStackEnvironmentDefaultLines } from './stack-environment-defaults.js';
|
|
3
6
|
import { parseStackEnvironmentFileLines } from './stack-environment-files.js';
|
|
4
7
|
import { parseStackLaunchLines } from './stack-launch.js';
|
|
5
8
|
import { parseStackWorkspaceSetupLines } from './stack-workspace-setup.js';
|
|
@@ -218,6 +221,14 @@ export function parseStackPieceSource(value, {
|
|
|
218
221
|
const deslop = textSection(all, 'Deslop');
|
|
219
222
|
if (!description) invalid(piecePath, 'Stack piece needs Description.');
|
|
220
223
|
const commands = parseStackCommandLines(all.get('Commands') || [], { path: piecePath });
|
|
224
|
+
const cityPresentation = parseStackCityPresentationLines(
|
|
225
|
+
all.has('City regions') ? all.get('City regions') : undefined,
|
|
226
|
+
{ path: piecePath },
|
|
227
|
+
);
|
|
228
|
+
const environmentDefaults = parseStackEnvironmentDefaultLines(
|
|
229
|
+
all.has('Environment defaults') ? all.get('Environment defaults') : undefined,
|
|
230
|
+
{ path: piecePath },
|
|
231
|
+
);
|
|
221
232
|
const environmentFiles = parseStackEnvironmentFileLines(
|
|
222
233
|
all.has('Environment files') ? all.get('Environment files') : undefined,
|
|
223
234
|
{ path: piecePath },
|
|
@@ -225,6 +236,10 @@ export function parseStackPieceSource(value, {
|
|
|
225
236
|
const launch = parseStackLaunchLines(all.has('Launch') ? all.get('Launch') : undefined, {
|
|
226
237
|
path: piecePath,
|
|
227
238
|
});
|
|
239
|
+
const deployment = parseStackDeploymentLines(
|
|
240
|
+
all.has('Deployment') ? all.get('Deployment') : undefined,
|
|
241
|
+
{ path: piecePath },
|
|
242
|
+
);
|
|
228
243
|
const workspaceSetup = parseStackWorkspaceSetupLines(
|
|
229
244
|
all.has('Workspace setup') ? all.get('Workspace setup') : undefined,
|
|
230
245
|
{ path: piecePath },
|
|
@@ -239,8 +254,12 @@ export function parseStackPieceSource(value, {
|
|
|
239
254
|
skill: skill(all, piecePath),
|
|
240
255
|
resources: resources(all, piecePath),
|
|
241
256
|
commands: commands.map(({ line: _line, ...command }) => command),
|
|
257
|
+
cityExclusions: cityPresentation.exclusions,
|
|
258
|
+
cityRegions: cityPresentation.regions,
|
|
259
|
+
environmentDefaults,
|
|
242
260
|
environmentFiles: environmentFiles || [],
|
|
243
261
|
launchTargets: launch?.targets || [],
|
|
262
|
+
deployment,
|
|
244
263
|
workspaceSetupSteps: workspaceSetup || [],
|
|
245
264
|
deslop,
|
|
246
265
|
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
const PREPARE_LINE = /^- Prepare `([^`\r\n]+)` with (.+?):[ \t]+(.+)$/u;
|
|
9
9
|
const RUNTIME_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
10
10
|
const READY_WHEN_SUFFIX = /^(.*?)[ \t]+when `([^`\r\n]+)` exists$/u;
|
|
11
|
+
const OPTIONAL_WHEN_SUFFIX = /^(.*?)[ \t]+if `([^`\r\n]+)` exists$/u;
|
|
11
12
|
const WORKDIR_SUFFIX = /^(.*?)[ \t]+in `([^`\r\n]+)`$/u;
|
|
12
13
|
|
|
13
14
|
function invalid(setupPath, message, line, details = {}) {
|
|
@@ -59,7 +60,7 @@ export function parseStackWorkspaceSetupLines(lines, {
|
|
|
59
60
|
if (!entry) {
|
|
60
61
|
invalid(
|
|
61
62
|
setupPath,
|
|
62
|
-
'Every Workspace setup entry must use `- Prepare `label` with `runtime` [in `workdir`] [when `path` exists]: `command` `argument`...`.',
|
|
63
|
+
'Every Workspace setup entry must use `- Prepare `label` with `runtime` [in `workdir`] [when|if `path` exists]: `command` `argument`...`.',
|
|
63
64
|
line,
|
|
64
65
|
{ observed: source },
|
|
65
66
|
);
|
|
@@ -69,9 +70,11 @@ export function parseStackWorkspaceSetupLines(lines, {
|
|
|
69
70
|
invalid(setupPath, 'Workspace setup labels must be non-empty single lines.', line);
|
|
70
71
|
}
|
|
71
72
|
const readyEntry = entry[2].match(READY_WHEN_SUFFIX);
|
|
72
|
-
const
|
|
73
|
+
const optionalEntry = readyEntry ? null : entry[2].match(OPTIONAL_WHEN_SUFFIX);
|
|
74
|
+
const setupSource = readyEntry ? readyEntry[1] : optionalEntry ? optionalEntry[1] : entry[2];
|
|
73
75
|
const readyWhen = readyEntry ? readyEntry[2] : '';
|
|
74
|
-
|
|
76
|
+
const runWhen = optionalEntry ? optionalEntry[2] : '';
|
|
77
|
+
if ((readyWhen || runWhen) && ((readyWhen || runWhen) === '.' || !isCanonicalProjectWorkdir(readyWhen || runWhen))) {
|
|
75
78
|
invalid(
|
|
76
79
|
setupPath,
|
|
77
80
|
'Workspace setup readiness paths must be canonical project-relative paths.',
|
|
@@ -98,6 +101,7 @@ export function parseStackWorkspaceSetupLines(lines, {
|
|
|
98
101
|
runtimeRequirements: parseRuntimeRequirements(runtimeSource, setupPath, line),
|
|
99
102
|
workdir,
|
|
100
103
|
...(readyWhen ? { readyWhen } : {}),
|
|
104
|
+
...(runWhen ? { runWhen } : {}),
|
|
101
105
|
});
|
|
102
106
|
}
|
|
103
107
|
if (steps.length === 0) {
|
package/src/index/stack.js
CHANGED
|
@@ -5,7 +5,12 @@ import { syncProjectSkills } from './agent-skills.js';
|
|
|
5
5
|
import { GenesisError } from './errors.js';
|
|
6
6
|
import { readBuiltinStackCatalog } from './stack-catalog.js';
|
|
7
7
|
import { parseStackCommandLines } from './stack-command.js';
|
|
8
|
+
import { composeStackCityPresentation } from './stack-city-presentation.js';
|
|
9
|
+
import { composeStackDeployment, parseStackDeploymentLines } from './stack-deployment.js';
|
|
8
10
|
import { resolveStackPieces } from './stack-composition.js';
|
|
11
|
+
import {
|
|
12
|
+
composeStackEnvironmentDefaults,
|
|
13
|
+
} from './stack-environment-defaults.js';
|
|
9
14
|
import {
|
|
10
15
|
composeStackEnvironmentFiles,
|
|
11
16
|
parseStackEnvironmentFileLines,
|
|
@@ -30,6 +35,7 @@ const STACK_SECTIONS = new Set([
|
|
|
30
35
|
'Environment files',
|
|
31
36
|
'Workspace setup',
|
|
32
37
|
'Commands',
|
|
38
|
+
'Deployment',
|
|
33
39
|
'Launch',
|
|
34
40
|
]);
|
|
35
41
|
const STACK_CUSTOMIZATION_ROOT = 'genesis/stack';
|
|
@@ -97,6 +103,7 @@ function withoutOuterBlankLines(lines = []) {
|
|
|
97
103
|
function renderStack({
|
|
98
104
|
commandLines = [],
|
|
99
105
|
componentIds: componentIdsValue,
|
|
106
|
+
deploymentLines = null,
|
|
100
107
|
environmentFileLines = null,
|
|
101
108
|
launchLines = null,
|
|
102
109
|
workspaceSetupLines = null,
|
|
@@ -114,6 +121,9 @@ function renderStack({
|
|
|
114
121
|
: ['', '## Workspace setup', '', ...withoutOuterBlankLines(workspaceSetupLines)]),
|
|
115
122
|
...(commandLines.length > 0 ? ['', '## Commands', ...commandLines] : []),
|
|
116
123
|
...(launchLines === null ? [] : ['', '## Launch', '', ...withoutOuterBlankLines(launchLines)]),
|
|
124
|
+
...(deploymentLines === null
|
|
125
|
+
? []
|
|
126
|
+
: ['', '## Deployment', '', ...withoutOuterBlankLines(deploymentLines)]),
|
|
117
127
|
'',
|
|
118
128
|
].join('\n');
|
|
119
129
|
}
|
|
@@ -152,6 +162,11 @@ export async function addStackPieces({ pieces, projectRoot }) {
|
|
|
152
162
|
);
|
|
153
163
|
const launchLines = sections.has('Launch') ? sections.get('Launch') : null;
|
|
154
164
|
parseStackLaunchLines(launchLines === null ? undefined : launchLines, { path: STACK_PATH });
|
|
165
|
+
const deploymentLines = sections.has('Deployment') ? sections.get('Deployment') : null;
|
|
166
|
+
parseStackDeploymentLines(
|
|
167
|
+
deploymentLines === null ? undefined : deploymentLines,
|
|
168
|
+
{ path: STACK_PATH },
|
|
169
|
+
);
|
|
155
170
|
const workspaceSetupLines = sections.has('Workspace setup')
|
|
156
171
|
? sections.get('Workspace setup')
|
|
157
172
|
: null;
|
|
@@ -162,6 +177,7 @@ export async function addStackPieces({ pieces, projectRoot }) {
|
|
|
162
177
|
const rendered = renderStack({
|
|
163
178
|
commandLines,
|
|
164
179
|
componentIds: selected,
|
|
180
|
+
deploymentLines,
|
|
165
181
|
environmentFileLines,
|
|
166
182
|
launchLines,
|
|
167
183
|
workspaceSetupLines,
|
|
@@ -249,7 +265,9 @@ export async function readStack(projectRoot) {
|
|
|
249
265
|
).map(({ line: _line, ...command }) => command);
|
|
250
266
|
const componentCommands = components.flatMap((piece) => piece.commands);
|
|
251
267
|
const commands = distinctCommands(projectCommands.length > 0 ? projectCommands : componentCommands);
|
|
268
|
+
const cityPresentation = composeStackCityPresentation(components);
|
|
252
269
|
const resources = resourceDeclarations(components);
|
|
270
|
+
const environmentDefaults = composeStackEnvironmentDefaults(components);
|
|
253
271
|
const projectEnvironmentFiles = parseStackEnvironmentFileLines(
|
|
254
272
|
sections.has('Environment files') ? sections.get('Environment files') : undefined,
|
|
255
273
|
{ path: STACK_PATH },
|
|
@@ -260,6 +278,11 @@ export async function readStack(projectRoot) {
|
|
|
260
278
|
{ path: STACK_PATH },
|
|
261
279
|
);
|
|
262
280
|
const launchTargets = composeStackLaunchTargets(components, projectLaunch);
|
|
281
|
+
const projectDeployment = parseStackDeploymentLines(
|
|
282
|
+
sections.has('Deployment') ? sections.get('Deployment') : undefined,
|
|
283
|
+
{ path: STACK_PATH },
|
|
284
|
+
);
|
|
285
|
+
const deployment = composeStackDeployment(components, projectDeployment);
|
|
263
286
|
const projectWorkspaceSetup = parseStackWorkspaceSetupLines(
|
|
264
287
|
sections.has('Workspace setup') ? sections.get('Workspace setup') : undefined,
|
|
265
288
|
{ path: STACK_PATH },
|
|
@@ -269,16 +292,24 @@ export async function readStack(projectRoot) {
|
|
|
269
292
|
path: STACK_PATH,
|
|
270
293
|
identityHash: sha256(stableJson({
|
|
271
294
|
components: components.map(({ id }) => id),
|
|
295
|
+
cityExclusions: cityPresentation.exclusions,
|
|
296
|
+
cityRegions: cityPresentation.regions,
|
|
272
297
|
commands: commands.map(({ label, argv }) => ({ label, argv })),
|
|
298
|
+
environmentDefaults,
|
|
273
299
|
environmentFiles,
|
|
274
300
|
launchTargets,
|
|
301
|
+
deployment,
|
|
275
302
|
resources,
|
|
276
303
|
workspaceSetup,
|
|
277
304
|
})),
|
|
278
305
|
components,
|
|
306
|
+
cityExclusions: cityPresentation.exclusions,
|
|
307
|
+
cityRegions: cityPresentation.regions,
|
|
279
308
|
commands,
|
|
309
|
+
environmentDefaults,
|
|
280
310
|
environmentFiles,
|
|
281
311
|
launchTargets,
|
|
312
|
+
deployment,
|
|
282
313
|
workspaceSetup,
|
|
283
314
|
resources,
|
|
284
315
|
guidance: composedProse(components, 'guidance'),
|
|
@@ -294,9 +325,13 @@ export function stackPromptContext(stack) {
|
|
|
294
325
|
...(piece.guidance ? { guidance: piece.guidance } : {}),
|
|
295
326
|
requires: piece.requires,
|
|
296
327
|
})),
|
|
328
|
+
cityExclusions: stack.cityExclusions,
|
|
329
|
+
cityRegions: stack.cityRegions,
|
|
297
330
|
verifyCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
|
|
331
|
+
environmentDefaults: stack.environmentDefaults,
|
|
298
332
|
environmentFiles: stack.environmentFiles,
|
|
299
333
|
launchTargets: stack.launchTargets,
|
|
334
|
+
deployment: stack.deployment,
|
|
300
335
|
workspaceSetup: stack.workspaceSetup,
|
|
301
336
|
};
|
|
302
337
|
}
|
package/src/index/utils.js
CHANGED
|
@@ -29,7 +29,7 @@ export function uniqueSorted(values = []) {
|
|
|
29
29
|
return [...new Set(values)].sort();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export async function writeFileAtomic(filePath, source, { mode =
|
|
32
|
+
export async function writeFileAtomic(filePath, source, { mode = 0o666 } = {}) {
|
|
33
33
|
const temporary = `${filePath}.${randomUUID()}.tmp`;
|
|
34
34
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
35
35
|
try {
|
|
@@ -2,8 +2,10 @@ import { runProcess } from './process.js';
|
|
|
2
2
|
import { asDiagnostic } from './errors.js';
|
|
3
3
|
import { gitContext } from './git.js';
|
|
4
4
|
import { clearVerification, writeVerification } from './project-state.js';
|
|
5
|
+
import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
|
|
5
6
|
import { missingStackResources } from './stack-preflight.js';
|
|
6
7
|
import { readStack } from './stack.js';
|
|
8
|
+
import { inspectWorkspaceSetupForStack } from './workspace-setup.js';
|
|
7
9
|
|
|
8
10
|
async function emit(onEvent, event) {
|
|
9
11
|
try { await onEvent?.(event); } catch { /* Progress observers do not control verification. */ }
|
|
@@ -17,7 +19,34 @@ export async function verifyProject({
|
|
|
17
19
|
} = {}) {
|
|
18
20
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
19
21
|
const stack = await readStack(root);
|
|
20
|
-
const
|
|
22
|
+
const workspaceSetup = await inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
23
|
+
if (workspaceSetup.status === 'blocked') {
|
|
24
|
+
return {
|
|
25
|
+
status: 'blocked',
|
|
26
|
+
summary: workspaceSetup.diagnostics.map(({ message }) => message).join(' '),
|
|
27
|
+
commands: [],
|
|
28
|
+
diagnostics: workspaceSetup.diagnostics,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const waiting = workspaceSetup.diagnostics.filter(
|
|
32
|
+
({ code }) => code === 'STACK_WORKSPACE_SETUP_WAITING',
|
|
33
|
+
);
|
|
34
|
+
if (waiting.length > 0) {
|
|
35
|
+
return {
|
|
36
|
+
status: 'unconfigured',
|
|
37
|
+
summary: waiting.map(({ message }) => message).join(' '),
|
|
38
|
+
commands: [],
|
|
39
|
+
diagnostics: waiting,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const resolvedEnvironment = withStackEnvironmentDefaults(
|
|
43
|
+
environment,
|
|
44
|
+
stack.environmentDefaults,
|
|
45
|
+
);
|
|
46
|
+
const missing = missingStackResources({
|
|
47
|
+
environment: resolvedEnvironment,
|
|
48
|
+
resources: stack.resources,
|
|
49
|
+
});
|
|
21
50
|
if (missing.length > 0) {
|
|
22
51
|
return {
|
|
23
52
|
status: 'blocked',
|
|
@@ -47,6 +76,7 @@ export async function verifyProject({
|
|
|
47
76
|
});
|
|
48
77
|
await processRunner(command.argv[0], command.argv.slice(1), {
|
|
49
78
|
cwd: root,
|
|
79
|
+
env: resolvedEnvironment,
|
|
50
80
|
maxBytes: 32 * 1024 * 1024,
|
|
51
81
|
code: 'VERIFICATION_FAILED',
|
|
52
82
|
});
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { asDiagnostic } from './errors.js';
|
|
4
5
|
import { gitContext } from './git.js';
|
|
6
|
+
import { runProcess } from './process.js';
|
|
5
7
|
import { readStack } from './stack.js';
|
|
8
|
+
import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
|
|
6
9
|
import { sha256, stableJson, uniqueSorted } from './utils.js';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const stack = await readStack(root);
|
|
11
|
+
async function emit(onEvent, event) {
|
|
12
|
+
try { await onEvent?.(event); } catch { /* Progress observers do not control preparation. */ }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function inspectWorkspaceSetupForStack({ projectRoot: root, stack }) {
|
|
14
16
|
const diagnostics = [...stack.workspaceSetup.diagnostics];
|
|
17
|
+
const applicableSteps = [];
|
|
15
18
|
if (diagnostics.length === 0) {
|
|
16
19
|
const waitingFor = [];
|
|
17
20
|
for (const step of stack.workspaceSetup.steps) {
|
|
21
|
+
if (step.runWhen) {
|
|
22
|
+
try {
|
|
23
|
+
await access(path.join(root, step.runWhen));
|
|
24
|
+
applicableSteps.push(step);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
applicableSteps.push(step);
|
|
18
31
|
if (!step.readyWhen) continue;
|
|
19
32
|
try {
|
|
20
33
|
await access(path.join(root, step.readyWhen));
|
|
@@ -36,9 +49,9 @@ export async function inspectProjectWorkspaceSetup({
|
|
|
36
49
|
const blocked = diagnostics.some(({ code }) => code !== 'STACK_WORKSPACE_SETUP_WAITING');
|
|
37
50
|
let status = 'unconfigured';
|
|
38
51
|
if (blocked) status = 'blocked';
|
|
39
|
-
else if (!waiting &&
|
|
52
|
+
else if (!waiting && applicableSteps.length > 0) status = 'ready';
|
|
40
53
|
const recipeHash = status === 'ready'
|
|
41
|
-
? sha256(stableJson({ version: 1, steps:
|
|
54
|
+
? sha256(stableJson({ version: 1, steps: applicableSteps }))
|
|
42
55
|
: '';
|
|
43
56
|
return {
|
|
44
57
|
status,
|
|
@@ -47,9 +60,87 @@ export async function inspectProjectWorkspaceSetup({
|
|
|
47
60
|
components: stack.components.map(({ id }) => id),
|
|
48
61
|
source: stack.workspaceSetup.source,
|
|
49
62
|
runtimeRequirements: uniqueSorted(
|
|
50
|
-
|
|
63
|
+
applicableSteps.flatMap((step) => step.runtimeRequirements),
|
|
51
64
|
),
|
|
52
|
-
steps:
|
|
65
|
+
steps: applicableSteps,
|
|
53
66
|
diagnostics,
|
|
54
67
|
};
|
|
55
68
|
}
|
|
69
|
+
|
|
70
|
+
/** Read the Stack's workspace preparation recipe without executing it. */
|
|
71
|
+
export async function inspectProjectWorkspaceSetup({
|
|
72
|
+
projectRoot,
|
|
73
|
+
} = {}) {
|
|
74
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
75
|
+
const stack = await readStack(root);
|
|
76
|
+
return inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Execute the selected Stack's exact workspace recipe with the caller's environment. */
|
|
80
|
+
export async function prepareProjectWorkspace({
|
|
81
|
+
environment = process.env,
|
|
82
|
+
onEvent,
|
|
83
|
+
processRunner = runProcess,
|
|
84
|
+
projectRoot,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
87
|
+
const stack = await readStack(root);
|
|
88
|
+
const setup = await inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
89
|
+
if (setup.status !== 'ready') {
|
|
90
|
+
const summary = setup.diagnostics.map(({ message }) => message).join(' ')
|
|
91
|
+
|| 'The selected Stack declares no applicable workspace preparation commands.';
|
|
92
|
+
return {
|
|
93
|
+
...setup,
|
|
94
|
+
summary,
|
|
95
|
+
commands: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resolvedEnvironment = withStackEnvironmentDefaults(
|
|
100
|
+
environment,
|
|
101
|
+
stack.environmentDefaults,
|
|
102
|
+
);
|
|
103
|
+
const commands = [];
|
|
104
|
+
try {
|
|
105
|
+
for (const step of setup.steps) {
|
|
106
|
+
await emit(onEvent, {
|
|
107
|
+
type: 'genesis.workspace-preparation',
|
|
108
|
+
code: 'WORKSPACE_PREPARATION_STARTED',
|
|
109
|
+
message: `Preparing: ${step.label}.`,
|
|
110
|
+
details: { label: step.label, argv: step.argv, workdir: step.workdir },
|
|
111
|
+
});
|
|
112
|
+
await processRunner(step.argv[0], step.argv.slice(1), {
|
|
113
|
+
cwd: path.resolve(root, step.workdir),
|
|
114
|
+
env: resolvedEnvironment,
|
|
115
|
+
maxBytes: 32 * 1024 * 1024,
|
|
116
|
+
code: 'WORKSPACE_PREPARATION_FAILED',
|
|
117
|
+
});
|
|
118
|
+
commands.push({
|
|
119
|
+
label: step.label,
|
|
120
|
+
argv: step.argv,
|
|
121
|
+
workdir: step.workdir,
|
|
122
|
+
});
|
|
123
|
+
await emit(onEvent, {
|
|
124
|
+
type: 'genesis.workspace-preparation',
|
|
125
|
+
code: 'WORKSPACE_PREPARATION_COMPLETED',
|
|
126
|
+
message: `Prepared: ${step.label}.`,
|
|
127
|
+
details: { label: step.label, argv: step.argv, workdir: step.workdir },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...setup,
|
|
132
|
+
status: 'passed',
|
|
133
|
+
summary: `Prepared the workspace with ${commands.length} command${commands.length === 1 ? '' : 's'}.`,
|
|
134
|
+
commands,
|
|
135
|
+
diagnostics: [],
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
...setup,
|
|
140
|
+
status: 'failed',
|
|
141
|
+
summary: error.message,
|
|
142
|
+
commands,
|
|
143
|
+
diagnostics: [asDiagnostic(error)],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,11 +7,15 @@ import { generateProjectPrompt } from './index/prompt.js';
|
|
|
7
7
|
import { initializeProject } from './index/init.js';
|
|
8
8
|
import { installCodexPlugin } from './index/codex-plugin.js';
|
|
9
9
|
import { inspectProjectEnvironment } from './index/environment-files.js';
|
|
10
|
+
import { inspectProjectDeployment } from './index/deployment.js';
|
|
10
11
|
import { inspectProjectLaunch } from './index/launch.js';
|
|
11
12
|
import { listBuiltinStackPieces } from './index/stack-catalog.js';
|
|
12
13
|
import { addStackPieces } from './index/stack.js';
|
|
13
14
|
import { verifyProject } from './index/verification.js';
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
inspectProjectWorkspaceSetup,
|
|
17
|
+
prepareProjectWorkspace,
|
|
18
|
+
} from './index/workspace-setup.js';
|
|
15
19
|
|
|
16
20
|
function withIndexResult(result, index) {
|
|
17
21
|
const changedFiles = [...new Set([...result.changedFiles, ...index.changedFiles])].sort();
|
|
@@ -77,10 +81,18 @@ export function inspectEnvironment(options) {
|
|
|
77
81
|
return inspectProjectEnvironment(options);
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
export function inspectDeployment(options) {
|
|
85
|
+
return inspectProjectDeployment(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
export function inspectWorkspaceSetup(options) {
|
|
81
89
|
return inspectProjectWorkspaceSetup(options);
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
export function prepareWorkspace(options) {
|
|
93
|
+
return prepareProjectWorkspace(options);
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
export function generatePrompt(options) {
|
|
85
97
|
return generateProjectPrompt(options);
|
|
86
98
|
}
|
|
@@ -13,6 +13,10 @@ JSKIT MySQL runtime, live-schema CRUD generation, and persistence conventions.
|
|
|
13
13
|
|
|
14
14
|
- `jskit-postgresql`
|
|
15
15
|
|
|
16
|
+
## Environment defaults
|
|
17
|
+
|
|
18
|
+
- Default `DB_CLIENT`: `mysql2`
|
|
19
|
+
|
|
16
20
|
## Resources
|
|
17
21
|
|
|
18
22
|
```json genesis-resource
|
|
@@ -24,7 +28,7 @@ JSKIT MySQL runtime, live-schema CRUD generation, and persistence conventions.
|
|
|
24
28
|
"required": ["DATABASE_URL"]
|
|
25
29
|
},
|
|
26
30
|
{
|
|
27
|
-
"required": ["
|
|
31
|
+
"required": ["DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD"],
|
|
28
32
|
"allowEmpty": ["DB_PASSWORD"]
|
|
29
33
|
}
|
|
30
34
|
]
|
|
@@ -13,6 +13,10 @@ JSKIT PostgreSQL runtime, live-schema CRUD generation, and persistence conventio
|
|
|
13
13
|
|
|
14
14
|
- `jskit-mysql`
|
|
15
15
|
|
|
16
|
+
## Environment defaults
|
|
17
|
+
|
|
18
|
+
- Default `DB_CLIENT`: `pg`
|
|
19
|
+
|
|
16
20
|
## Resources
|
|
17
21
|
|
|
18
22
|
```json genesis-resource
|
|
@@ -24,7 +28,7 @@ JSKIT PostgreSQL runtime, live-schema CRUD generation, and persistence conventio
|
|
|
24
28
|
"required": ["DATABASE_URL"]
|
|
25
29
|
},
|
|
26
30
|
{
|
|
27
|
-
"required": ["
|
|
31
|
+
"required": ["DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD"],
|
|
28
32
|
"allowEmpty": ["DB_PASSWORD"]
|
|
29
33
|
}
|
|
30
34
|
]
|
package/stacks/pieces/jskit.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Description
|
|
4
4
|
|
|
5
|
-
JSKIT
|
|
6
|
-
|
|
5
|
+
AI-first JSKIT runtime capabilities, tested source patterns, Vue surfaces, and
|
|
6
|
+
application-owned composition.
|
|
7
7
|
|
|
8
8
|
## Requires
|
|
9
9
|
|
|
@@ -18,7 +18,7 @@ generated files, and runtime composition.
|
|
|
18
18
|
|
|
19
19
|
- Before creating or modifying JSKIT Vue/Vuetify UI, load the installed
|
|
20
20
|
`jskit` Agent Skill and read its `references/material-3.md` completely.
|
|
21
|
-
- Implement Material 3 through JSKIT
|
|
21
|
+
- Implement Material 3 through JSKIT patterns, shared screens, shell and
|
|
22
22
|
placement seams, app-owned theme configuration, and supported Vuetify APIs.
|
|
23
23
|
- Use geometry-preserving Material skeletons for all user-visible loading;
|
|
24
24
|
never use a generic spinner or circular progress indicator, or let content
|
|
@@ -30,11 +30,23 @@ generated files, and runtime composition.
|
|
|
30
30
|
## Workspace setup
|
|
31
31
|
|
|
32
32
|
- Prepare `Install dependencies` with `nodejs` when `package.json` exists: `npm` `install`
|
|
33
|
+
- Prepare `Prepare database` with `nodejs` if `scripts/prepare-database.js` exists: `npm` `run` `db:prepare`
|
|
33
34
|
|
|
34
35
|
## Environment files
|
|
35
36
|
|
|
36
37
|
- Dotenv `.env`
|
|
37
38
|
|
|
39
|
+
## City regions
|
|
40
|
+
|
|
41
|
+
- Ignore `**/test/**`
|
|
42
|
+
- Ignore `**/tests/**`
|
|
43
|
+
- Ignore `**/__tests__/**`
|
|
44
|
+
- Ignore `**/*.test.*`
|
|
45
|
+
- Ignore `**/*.spec.*`
|
|
46
|
+
- Match `packages` as `Packages`: `packages/**`
|
|
47
|
+
- Match `source` as `Source`: `src/**`
|
|
48
|
+
- Fallback `everything-else` as `Everything else`
|
|
49
|
+
|
|
38
50
|
## Launch
|
|
39
51
|
|
|
40
52
|
### Target `app`: Run app
|
|
@@ -42,21 +54,30 @@ generated files, and runtime composition.
|
|
|
42
54
|
- Default.
|
|
43
55
|
- Preferred port: `3000`
|
|
44
56
|
- URL path: `/`
|
|
57
|
+
- Ready when: `GET` `/api/health` returns `200`
|
|
58
|
+
- Runtimes: `nodejs`
|
|
59
|
+
- Serve `Develop`: `npm` `run` `develop`
|
|
60
|
+
|
|
61
|
+
## Deployment
|
|
62
|
+
|
|
45
63
|
- Runtimes: `nodejs`
|
|
46
|
-
-
|
|
64
|
+
- Ready when: `GET` `/api/health` returns `200`
|
|
65
|
+
- Prepare `Install dependencies`: `npm` `install`
|
|
66
|
+
- Build `Build`: `npm` `run` `build`
|
|
67
|
+
- Migrate `Prepare database`: `npm` `run` `db:prepare`
|
|
47
68
|
- Serve `Start`: `npm` `start`
|
|
48
69
|
|
|
49
70
|
## Deslop
|
|
50
71
|
|
|
51
72
|
- For every affected JSKIT screen, load the installed `jskit` Agent Skill,
|
|
52
73
|
read `references/material-3.md`, and execute its Material 3 audit.
|
|
53
|
-
- Use JSKIT
|
|
54
|
-
|
|
55
|
-
- Treat
|
|
56
|
-
placements, and infrastructure tests coherently; when replacing a
|
|
74
|
+
- Use JSKIT patterns, packages, resources, composables, screens, and placement
|
|
75
|
+
seams instead of parallel framework plumbing.
|
|
76
|
+
- Treat copied pattern source as app-owned and customizable. Adapt routes,
|
|
77
|
+
placements, and infrastructure tests coherently; when replacing a foundation
|
|
57
78
|
route, update its baseline browser coverage instead of deleting it.
|
|
58
79
|
- Consolidate repeated validation, pending/error state, API access, and page
|
|
59
|
-
behavior at the
|
|
80
|
+
behavior at the shared resource or established JSKIT seam.
|
|
60
81
|
- Replace transient command-error alerts that shift page content with the
|
|
61
82
|
established JSKIT action feedback/snackbar seam; preserve stable in-page
|
|
62
83
|
resource-load errors, retry states, and field validation.
|