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.
@@ -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
@@ -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 setupSource = readyEntry ? readyEntry[1] : entry[2];
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
- if (readyWhen && (readyWhen === '.' || !isCanonicalProjectWorkdir(readyWhen))) {
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) {
@@ -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
  }
@@ -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 = 0o644 } = {}) {
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 missing = missingStackResources({ environment, resources: stack.resources });
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
- /** Read the Stack's workspace preparation recipe without executing it. */
9
- export async function inspectProjectWorkspaceSetup({
10
- projectRoot,
11
- } = {}) {
12
- const root = (await gitContext(projectRoot)).repositoryRoot;
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 && stack.workspaceSetup.steps.length > 0) status = 'ready';
52
+ else if (!waiting && applicableSteps.length > 0) status = 'ready';
40
53
  const recipeHash = status === 'ready'
41
- ? sha256(stableJson({ version: 1, steps: stack.workspaceSetup.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
- stack.workspaceSetup.steps.flatMap((step) => step.runtimeRequirements),
63
+ applicableSteps.flatMap((step) => step.runtimeRequirements),
51
64
  ),
52
- steps: stack.workspaceSetup.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 { inspectProjectWorkspaceSetup } from './index/workspace-setup.js';
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": ["DB_CLIENT", "DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD"],
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": ["DB_CLIENT", "DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD"],
31
+ "required": ["DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD"],
28
32
  "allowEmpty": ["DB_PASSWORD"]
29
33
  }
30
34
  ]
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## Description
4
4
 
5
- JSKIT application creation, package generators, Vue surfaces, app-owned
6
- generated files, and runtime composition.
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 generators, shared screens, shell and
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
- - Prepare `Build`: `npm` `run` `build`
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 generators, packages, resources, composables, screens, and
54
- placement seams instead of parallel framework plumbing.
55
- - Treat generated output as app-owned and customizable. Adapt routes,
56
- placements, and infrastructure tests coherently; when replacing a starter
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 generated shared resource or established JSKIT seam.
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.