genesis-compiler 1.2.1 → 1.2.2

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 CHANGED
@@ -13,7 +13,7 @@ interaction.
13
13
  ```text
14
14
  genesis/
15
15
  blueprint.md non-technical product intent
16
- stack.md selected technology guidance, checks, setup, and launch data
16
+ stack.md selected technology guidance, checks, setup, launch, and deployment data
17
17
  stack/ optional per-component Description, Guidance, and Deslop customization
18
18
  program/ concise explanations grouped into conceptual subsystems
19
19
  .agents/skills/
@@ -131,25 +131,51 @@ Genesis does not run the recipe. Stack resources such as database variables do
131
131
  not gate dependency installation.
132
132
 
133
133
  Hosts that manage project environments can call `inspectEnvironment()`.
134
+ Concrete Stack integrations may declare public, non-secret Environment
135
+ defaults such as the database driver selected by that integration. Explicit
136
+ host or user values take precedence. The built-in JSKIT MySQL and PostgreSQL
137
+ pieces respectively declare `DB_CLIENT=mysql2` and `DB_CLIENT=pg`, so users
138
+ provide connection values rather than restating a deterministic technology
139
+ choice.
134
140
  An optional readable `## Environment files` section declares safe
135
141
  project-relative dotenv projections such as `.env`. A project declaration
136
142
  replaces component defaults, including with `- Nothing.`; otherwise component
137
- paths compose. Genesis reports resource declarations, value-free missing-input
138
- diagnostics, and paths. It never returns values, writes files, stores secrets,
139
- or decides which resolved values a host should materialize.
143
+ paths compose. Genesis reports public defaults, resource declarations,
144
+ value-free missing-input diagnostics, and paths. It never returns supplied
145
+ environment values, writes files, stores secrets, or decides which resolved
146
+ host values should be materialized.
140
147
 
141
148
  Hosts that provide previews can call the public `inspectLaunch()` API. An
142
149
  optional readable `## Launch` section in `genesis/stack.md` declares labeled
143
150
  targets, exact backticked argument tokens, relative working directories,
144
151
  abstract runtime requirements, `{host}`/`{port}` substitutions, and an optional
145
152
  application-owned Preview identity block. Concrete components may provide a
146
- default: `jskit` declares build followed by `npm start`; generic `nodejs` and
147
- `php` do not guess. Genesis starts nothing itself. It returns preview identity
153
+ default: `jskit` declares its app-owned `npm run develop` command; generic
154
+ `nodejs` and `php` do not guess. Genesis starts nothing itself. It returns preview identity
148
155
  command metadata and environment variable names, never their values. The host
149
- continues to own pinned runtimes, ports, processes, readiness, identity-command
150
- execution and secrets, browser binaries, Git, and credentials. See
156
+ continues to own pinned runtimes, ports, processes, readiness probing and
157
+ timeouts, identity-command execution and secrets, browser binaries, Git, and
158
+ credentials. Genesis owns only the exact HTTP readiness predicate declared by
159
+ the selected Stack. See
151
160
  [`docs/stack-components.md`](docs/stack-components.md) for the v1 contract.
152
161
 
162
+ Hosts that publish applications can call the separate public
163
+ `inspectDeployment()` API. An optional readable `## Deployment` section
164
+ declares an ordered production recipe: project-relative workdir, abstract
165
+ runtimes, exact Prepare/Build/Migrate/Serve argv, and one HTTP readiness
166
+ predicate. A concrete component may provide the recipe; the built-in `jskit`
167
+ piece declares production dependency installation, build, database preparation,
168
+ `npm start`, and `/api/health`. Generic language pieces do not guess. A project
169
+ section replaces component defaults wholesale, and `- Nothing.` explicitly
170
+ disables publishing.
171
+
172
+ Genesis only validates and reports this recipe and the Stack's abstract resource
173
+ requirements. It does not provision a database, resolve credentials, copy a
174
+ release, run commands, start a service, reserve a port, route traffic, issue TLS,
175
+ or roll back. A deployment host owns all of those operational responsibilities,
176
+ including the mapping from `mysql` or `postgresql` resources to its managed
177
+ services.
178
+
153
179
  At session start, Codex receives only a short explanation of how Genesis is
154
180
  organized. After it locates the source involved in a request, it can load the
155
181
  specific explanatory and technology context with:
@@ -332,8 +358,11 @@ Run the Stack's declared checks explicitly:
332
358
  genesis verify
333
359
  ```
334
360
 
335
- Genesis first evaluates generic Stack resource declarations, then runs each
336
- declared command without a shell. A complete successful run writes only
361
+ Genesis first waits for any technology-declared Workspace setup marker. A
362
+ missing marker is `unconfigured`, not a failed command. Once the workspace
363
+ exists, Genesis applies public Stack defaults beneath explicit environment
364
+ values, evaluates generic resource declarations, and runs each declared
365
+ command without a shell. A complete successful run writes only
337
366
  `.genesis/verification.json`, recording:
338
367
 
339
368
  - the exact Git-visible code hash;
@@ -413,10 +442,10 @@ to execute with its own pinned runtimes, caches, process isolation, and
413
442
  credentials. A configured recipe may remain unconfigured while it waits for a
414
443
  technology-declared project marker.
415
444
  `inspectEnvironment()` returns normalized Stack resource declarations,
416
- value-free missing-input diagnostics, and dotenv projection paths without
417
- returning any environment value.
445
+ public non-secret defaults, value-free missing-input diagnostics, and dotenv
446
+ projection paths without returning any supplied environment value.
418
447
  `inspectLaunch()` returns a validated, normalized Stack launch declaration for
419
- a host to execute with its own runtime, port, process, readiness, preview
448
+ a host to execute with its own runtime, port, process, readiness probe, preview
420
449
  identity, secret, and browser policy.
421
450
 
422
451
  A host such as Vibe64 can send the generated prompt to its existing agent:
@@ -37,6 +37,10 @@ What this component contributes.
37
37
  }
38
38
  ```
39
39
 
40
+ ## Environment defaults
41
+
42
+ - Default `EXAMPLE_DRIVER`: `example`
43
+
40
44
  ## Guidance
41
45
 
42
46
  ### Technology-specific behavior
@@ -69,6 +73,7 @@ What this component contributes.
69
73
  - Workdir: `.`
70
74
  - Preferred port: `3000`
71
75
  - URL path: `/`
76
+ - Ready when: `GET` `/api/health` returns `200`
72
77
  - Runtimes: `nodejs`
73
78
  - Prepare `Build`: `npm` `run` `build`
74
79
  - Serve `Start`: `npm` `run` `server` `--` `--host` `{host}` `--port` `{port}`
@@ -93,7 +98,9 @@ from Genesis or from the declared npm package. Its `SKILL.md`, `references/`,
93
98
  Genesis expanding every manual into every prompt. `Resources` declares generic
94
99
  alternative environment-name sets reported by prompt generation and checked
95
100
  before verification. `allowEmpty` may name a required variable whose empty
96
- string is valid. `Guidance` is supplemental prose used by work, review, Program,
101
+ string is valid. `Environment defaults` declares public non-secret constants
102
+ owned by a concrete integration; explicit host values take precedence.
103
+ `Guidance` is supplemental prose used by work, review, Program,
97
104
  description, and cleanup prompts, as well as path-focused context. `Deslop`
98
105
  adds cleanup-only prose to explicit cleanup and the automatic Codex cleanup
99
106
  continuation. With no selected component, only the `genesis-deslop` skill
@@ -132,6 +139,16 @@ Precedence is intentionally small and deterministic:
132
139
  General Stack resources do not gate Workspace setup. In particular, missing
133
140
  database variables do not prevent an otherwise independent dependency install.
134
141
 
142
+ Each `Environment defaults` entry is one `Default` bullet with a backticked
143
+ environment name and value. Selected
144
+ component defaults compose by name; identical declarations collapse and
145
+ different values fail rather than silently choosing one. Defaults participate
146
+ in Stack identity, resource preflight, prompt context, Launch inspection, and
147
+ verification execution. The JSKIT database integrations use this contract for
148
+ their deterministic `DB_CLIENT` driver ids; generic database pieces do not.
149
+ Secrets and deployment-specific connection values remain host inputs and must
150
+ never be declared as defaults.
151
+
135
152
  `Environment files` optionally declares project-relative dotenv projections a
136
153
  host may materialize from its own resolved project environment. Genesis
137
154
  validates and returns only the format and path; it never reads environment
@@ -145,8 +162,9 @@ The declaration is useful independently of Launch and Workspace setup.
145
162
  offer a preview without guessing how a concrete technology starts. Repeat a
146
163
  ``### Target `id`: label`` block to declare several targets. Default, Workdir,
147
164
  Preferred port, URL path, and Runtimes are optional; their normalized defaults
148
- are false, `.`, null, `/`, and no runtime requirements. Every Prepare or Serve
149
- command and argument is a separate backticked value returned as an `argv`
165
+ are false, `.`, null, `/`, and no runtime requirements. Every target declares
166
+ one HTTP readiness predicate with a project path and an exact successful status
167
+ from 200 through 399. Every Prepare or Serve command and argument is a separate backticked value returned as an `argv`
150
168
  array, never a shell program. A target has exactly one final Serve step; any
151
169
  earlier steps must be Prepare steps. Workdir is canonical and project-relative,
152
170
  Preferred port is only a hint from 1024 through 65535, and URL path is an
@@ -155,7 +173,8 @@ application path rather than a URL. The only substitutions are `{host}` and
155
173
  Launch section to disable component targets.
156
174
 
157
175
  Genesis validates and reports Launch data but does not select a runtime,
158
- reserve a port, start a process, decide readiness, or control a browser.
176
+ reserve a port, start a process, probe readiness, or control a browser. The host
177
+ executes the declared predicate and owns its timeout and retry policy.
159
178
 
160
179
  The optional `#### Preview identity` block is specific to hosts that implement
161
180
  the declared `vibe64.preview-identity.command.v1` protocol. Its executable is a
@@ -176,7 +195,41 @@ the ids it supports to its own pinned runtime packs; for example, it may map
176
195
  dependencies such as `composer`, or treats an unknown id as something similar.
177
196
  Declare each independently required runtime. Generic language pieces such as
178
197
  `nodejs` and `php` provide no Launch recipe. A concrete piece such as `jskit`
179
- may declare one; Genesis does not invent missing targets.
198
+ may declare its app-owned `npm run develop` command; Genesis does not invent
199
+ missing targets or supervise framework-specific subprocesses.
200
+
201
+ `Deployment` is a separate optional readable production recipe. It must not be
202
+ inferred from Launch because preview preparation and production publication have
203
+ different safety, dependency, migration, artifact, and rollback concerns. Its
204
+ complete form is:
205
+
206
+ ```markdown
207
+ ## Deployment
208
+
209
+ - Workdir: `.`
210
+ - Runtimes: `nodejs`
211
+ - Ready when: `GET` `/api/health` returns `200`
212
+ - Prepare `Install production dependencies`: `npm` `install` `--omit=dev`
213
+ - Build `Build`: `npm` `run` `build`
214
+ - Migrate `Prepare database`: `npm` `run` `db:prepare`
215
+ - Serve `Start`: `npm` `start`
216
+ ```
217
+
218
+ Every command and argument is a separate backticked value returned as an argv
219
+ array; shell source is rejected. Workdir is canonical and project-relative.
220
+ Prepare, Build, and Migrate steps are optional and retain their declared order.
221
+ There is exactly one final Serve step and one exact HTTP readiness predicate
222
+ whose successful status is from 200 through 399. Runtime ids remain abstract.
223
+
224
+ A project `## Deployment` section replaces every component recipe as one unit,
225
+ including with `- Nothing.`. Without a project section, exactly one selected
226
+ component recipe may apply; several are blocked as ambiguous and never merged.
227
+ Genesis returns the Stack's existing resource declarations with the recipe but
228
+ does not check production values, provision services, execute commands, package
229
+ artifacts, start processes, probe health, route traffic, manage TLS, or retain
230
+ releases. Those are host responsibilities. This keeps the same project portable:
231
+ Genesis alone explains the production requirements, while a host such as Vibe64
232
+ Online can map `mysql` or `postgresql` to a pinned managed service.
180
233
 
181
234
  `Indexers` selects installed structural adapters for the derived Machine City.
182
235
  The shared ast-grep engine owns traversal, bounds, diagnostics, JSON, and
@@ -230,7 +283,7 @@ Replacement component description.
230
283
  Each field is optional. `Override` replaces only that installed field; `Add`
231
284
  then follows the installed or overridden value. These files cannot alter
232
285
  dependencies, conflicts, Agent Skill source, indexers, resources, environment
233
- files, commands, Workspace setup, or Launch declarations.
286
+ defaults, environment files, commands, Workspace setup, or Launch declarations.
234
287
  `genesis stack add` preserves the files and composes their effective prose after
235
288
  dependency resolution.
236
289
 
@@ -248,8 +301,8 @@ rather than a prompt or controller special case.
248
301
  Description, Guidance, Agent Skill, and Deslop prose are instructions, not
249
302
  verification evidence. Changes to them do not stale an exact successful
250
303
  verification record; selected component ids, resources, environment files,
251
- Workspace setup, launch targets, and verification commands remain part of the
252
- Stack identity.
304
+ environment defaults, Workspace setup, launch targets, and verification
305
+ commands remain part of the Stack identity.
253
306
 
254
307
  Hosts inspect normalized environment requirements independently:
255
308
 
@@ -260,9 +313,10 @@ const projectEnvironment = await inspectEnvironment({ environment, projectRoot }
260
313
  ```
261
314
 
262
315
  The result has `status` (`ready`, `missing-inputs`, or `unconfigured`),
263
- `stackHash`, selected `components`, declared `resources`, value-free
264
- `diagnostics`, and normalized `files`. Each file contains `format`, `path`, and
265
- `source`. Environment values never cross this API boundary.
316
+ `stackHash`, selected `components`, public non-secret `environmentDefaults`,
317
+ declared `resources`, value-free `diagnostics`, and normalized `files`. Each default has
318
+ `name`, `value`, and component `sources`; each file contains `format`, `path`,
319
+ and `source`. Supplied environment values never cross this API boundary.
266
320
 
267
321
  Fresh-workspace hosts read setup from the package root:
268
322
 
@@ -292,11 +346,12 @@ const launch = await inspectLaunch({ projectRoot, environment });
292
346
  ```
293
347
 
294
348
  The result has `status` (`ready`, `blocked`, or `unconfigured`), `stackHash`,
295
- selected `components`, distinct `runtimeRequirements`, declared `resources`,
349
+ selected `components`, public `environmentDefaults`, distinct
350
+ `runtimeRequirements`, declared `resources`,
296
351
  normalized `targets`, and resource `diagnostics`. Each target reports its
297
352
  `source`, `available`, and `disabledReason`, plus its normalized
298
- `previewIdentity` declaration when present. Environment values are inspected
299
- but never returned. Here, `available` means only that the Stack's declared
353
+ `previewIdentity` declaration when present. Supplied environment values are
354
+ inspected but never returned. Here, `available` means only that the Stack's declared
300
355
  resource inputs are present: the host must still reject runtime requirements it
301
356
  cannot map. `unconfigured` means there are no targets, `blocked` means declared
302
357
  resource inputs are missing, and `ready` means neither condition applies. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with optional Codex hooks.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
package/prompts/start.txt CHANGED
@@ -13,6 +13,11 @@ the non-technical Blueprint from the user's answers before implementation.
13
13
  If the product direction becomes clear while Stack is still empty, do not end
14
14
  the turn after only writing or summarizing the Blueprint: present the relevant
15
15
  Stack choices in that same turn and ask the user to select one explicitly.
16
+ This opening turn owns product direction and Stack selection, not final
17
+ verification. Do not run `genesis verify` before implementation has created
18
+ the selected technology's workspace substrate. If structural validation is
19
+ useful, run `genesis check`; treat missing workspace markers as unconfigured,
20
+ not as an implementation failure.
16
21
 
17
22
  When `projectKind` is `existing`, ask what the user wants to understand or
18
23
  change. Use the current Blueprint, Stack, Program, and code index when present.
@@ -48,6 +48,9 @@ Neither is authority or proof; both may be regenerated with `genesis index`.
48
48
 
49
49
  ## Verify and report
50
50
 
51
- Use `genesis verify` for the Stack's declared final checks. Report files
52
- changed, checks actually run, and anything still requiring attention. Never
53
- claim that an unrun check passed or that passing checks prove the whole product.
51
+ After the selected technology's workspace substrate exists, use `genesis
52
+ verify` for the Stack's declared final checks. An unconfigured result means the
53
+ declared workspace or checks do not exist yet; it is not a failing check. Report
54
+ files changed, checks actually run, and anything still requiring attention.
55
+ Never claim that an unrun check passed or that passing checks prove the whole
56
+ product.
@@ -6,6 +6,7 @@ import { gitContext } from './git.js';
6
6
  import { inspectProgram } from './program.js';
7
7
  import { inspectVerification } from './project-state.js';
8
8
  import { missingStackResources } from './stack-preflight.js';
9
+ import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
9
10
 
10
11
  function invalidResult(area, error) {
11
12
  return {
@@ -55,7 +56,10 @@ export async function checkProject({ environment = process.env, projectRoot } =
55
56
  } catch (error) {
56
57
  skills = { status: 'invalid', diagnostics: [asDiagnostic(error)], skills: [] };
57
58
  }
58
- const missingResources = missingStackResources({ environment, resources: stack.resources });
59
+ const missingResources = missingStackResources({
60
+ environment: withStackEnvironmentDefaults(environment, stack.environmentDefaults),
61
+ resources: stack.resources,
62
+ });
59
63
  const verification = await inspectVerification({ projectRoot: root, stack });
60
64
  const diagnostics = [
61
65
  ...(program.diagnostic ? [program.diagnostic] : []),
@@ -0,0 +1,31 @@
1
+ import { gitContext } from './git.js';
2
+ import { readStack } from './stack.js';
3
+ import { sha256, stableJson, uniqueSorted } from './utils.js';
4
+
5
+ /** Read the Stack's production recipe without provisioning or publishing anything. */
6
+ export async function inspectProjectDeployment({ projectRoot } = {}) {
7
+ const root = (await gitContext(projectRoot)).repositoryRoot;
8
+ const stack = await readStack(root);
9
+ const diagnostics = [...stack.deployment.diagnostics];
10
+ let status = 'unconfigured';
11
+ if (diagnostics.length > 0) status = 'blocked';
12
+ else if (stack.deployment.steps.length > 0) status = 'ready';
13
+ const recipe = {
14
+ version: stack.deployment.version,
15
+ workdir: stack.deployment.workdir,
16
+ runtimeRequirements: stack.deployment.runtimeRequirements,
17
+ readiness: stack.deployment.readiness,
18
+ steps: stack.deployment.steps,
19
+ };
20
+ return {
21
+ status,
22
+ stackHash: stack.identityHash,
23
+ recipeHash: status === 'ready' ? sha256(stableJson(recipe)) : '',
24
+ components: stack.components.map(({ id }) => id),
25
+ source: stack.deployment.source,
26
+ runtimeRequirements: uniqueSorted(stack.deployment.runtimeRequirements),
27
+ resources: stack.resources,
28
+ ...recipe,
29
+ diagnostics,
30
+ };
31
+ }
@@ -1,17 +1,24 @@
1
1
  import { gitContext } from './git.js';
2
2
  import { readStack } from './stack.js';
3
+ import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
3
4
  import { missingStackResources } from './stack-preflight.js';
4
5
 
5
- /** Inspect Stack environment requirements without returning or writing values. */
6
+ /** Inspect Stack environment requirements without returning supplied host values. */
6
7
  export async function inspectProjectEnvironment({ environment = process.env, projectRoot } = {}) {
7
8
  const root = (await gitContext(projectRoot)).repositoryRoot;
8
9
  const stack = await readStack(root);
9
- const diagnostics = missingStackResources({ environment, resources: stack.resources });
10
- const configured = stack.environmentFiles.length > 0 || stack.resources.length > 0;
10
+ const diagnostics = missingStackResources({
11
+ environment: withStackEnvironmentDefaults(environment, stack.environmentDefaults),
12
+ resources: stack.resources,
13
+ });
14
+ const configured = stack.environmentDefaults.length > 0
15
+ || stack.environmentFiles.length > 0
16
+ || stack.resources.length > 0;
11
17
  return {
12
18
  status: !configured ? 'unconfigured' : diagnostics.length > 0 ? 'missing-inputs' : 'ready',
13
19
  stackHash: stack.identityHash,
14
20
  components: stack.components.map(({ id }) => id),
21
+ environmentDefaults: stack.environmentDefaults,
15
22
  files: stack.environmentFiles,
16
23
  resources: stack.resources,
17
24
  diagnostics,
@@ -1,5 +1,6 @@
1
1
  import { gitContext } from './git.js';
2
2
  import { missingStackResources } from './stack-preflight.js';
3
+ import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
3
4
  import { readStack } from './stack.js';
4
5
  import { uniqueSorted } from './utils.js';
5
6
 
@@ -10,7 +11,10 @@ export async function inspectProjectLaunch({
10
11
  } = {}) {
11
12
  const root = (await gitContext(projectRoot)).repositoryRoot;
12
13
  const stack = await readStack(root);
13
- const diagnostics = missingStackResources({ environment, resources: stack.resources });
14
+ const diagnostics = missingStackResources({
15
+ environment: withStackEnvironmentDefaults(environment, stack.environmentDefaults),
16
+ resources: stack.resources,
17
+ });
14
18
  const disabledReason = diagnostics.length === 0
15
19
  ? null
16
20
  : diagnostics.map(({ message }) => message).join(' ');
@@ -26,6 +30,7 @@ export async function inspectProjectLaunch({
26
30
  status,
27
31
  stackHash: stack.identityHash,
28
32
  components: stack.components.map(({ id }) => id),
33
+ environmentDefaults: stack.environmentDefaults,
29
34
  runtimeRequirements: uniqueSorted(targets.flatMap((target) => target.runtimeRequirements)),
30
35
  resources: stack.resources,
31
36
  targets,
@@ -9,6 +9,7 @@ import { gitContext } from './git.js';
9
9
  import { inspectProgram } from './program.js';
10
10
  import { inspectVerification } from './project-state.js';
11
11
  import { missingStackResources } from './stack-preflight.js';
12
+ import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
12
13
  import { stableJson } from './utils.js';
13
14
  import { gitVisibleFileStates } from './project-files.js';
14
15
  import { isProjectContentPath } from './paths.js';
@@ -305,7 +306,10 @@ export async function generateProjectPrompt({
305
306
  buildProjectIndex({ projectRoot: root, write: false }),
306
307
  inspectProjectSkills({ projectRoot: root, stack }),
307
308
  ]);
308
- const missing = missingStackResources({ environment, resources: stack.resources });
309
+ const missing = missingStackResources({
310
+ environment: withStackEnvironmentDefaults(environment, stack.environmentDefaults),
311
+ resources: stack.resources,
312
+ });
309
313
  const warnings = [
310
314
  ...(program.diagnostic ? [program.diagnostic] : []),
311
315
  ...missing,
@@ -64,6 +64,7 @@ export async function listBuiltinStackPieces() {
64
64
  requires: piece.requires,
65
65
  conflicts: piece.conflicts,
66
66
  indexers: piece.indexers,
67
+ environmentDefaults: piece.environmentDefaults,
67
68
  launchTargets: piece.launchTargets.map(({ id }) => id),
68
69
  workspaceSetup: piece.workspaceSetupSteps,
69
70
  skill: piece.skill === null ? null : piece.skill.path,
@@ -0,0 +1,204 @@
1
+ import { GenesisError } from './errors.js';
2
+ import {
3
+ isCanonicalProjectWorkdir,
4
+ isSafeProcessExecutable,
5
+ parseBacktickedArguments,
6
+ } from './stack-process.js';
7
+
8
+ const RUNTIME_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
9
+ const STEP_LINE = /^- (Prepare|Build|Migrate|Serve) `([^`\r\n]+)`:[ \t]+(.+)$/u;
10
+ const STEP_ROLE_ORDER = Object.freeze({
11
+ prepare: 0,
12
+ build: 1,
13
+ migrate: 2,
14
+ serve: 3,
15
+ });
16
+
17
+ function invalid(deploymentPath, message, line, details = {}) {
18
+ throw new GenesisError('STACK_DEPLOYMENT_INVALID', message, {
19
+ path: deploymentPath,
20
+ ...(line === undefined ? {} : { line }),
21
+ ...details,
22
+ });
23
+ }
24
+
25
+ function oneToken(source, prefix, deploymentPath, label, line) {
26
+ const values = parseBacktickedArguments(source.slice(prefix.length));
27
+ if (!values || values.length !== 1) {
28
+ invalid(deploymentPath, `${label} accepts exactly one backticked value.`, line);
29
+ }
30
+ return values[0];
31
+ }
32
+
33
+ function parseRuntimes(source, deploymentPath, line) {
34
+ const values = parseBacktickedArguments(source.slice('- Runtimes: '.length));
35
+ if (!values || values.some((value) => !RUNTIME_ID.test(value))) {
36
+ invalid(
37
+ deploymentPath,
38
+ 'Deployment runtimes must be separate backticked technology ids.',
39
+ line,
40
+ );
41
+ }
42
+ if (new Set(values).size !== values.length) {
43
+ invalid(deploymentPath, 'Deployment contains a duplicate runtime.', line);
44
+ }
45
+ return values;
46
+ }
47
+
48
+ function parseUrlPath(value, deploymentPath, line) {
49
+ const normalized = String(value || '').trim();
50
+ if (!normalized.startsWith('/') || normalized.startsWith('//') || /[\\?#]/u.test(normalized)) {
51
+ invalid(
52
+ deploymentPath,
53
+ 'Deployment readiness path must begin with one slash and contain no query or fragment.',
54
+ line,
55
+ );
56
+ }
57
+ return normalized;
58
+ }
59
+
60
+ function parseReadiness(source, deploymentPath, line) {
61
+ const match = source.match(/^- Ready when: `GET` `([^`\r\n]+)` returns `([0-9]{3})`$/u);
62
+ if (!match) {
63
+ invalid(
64
+ deploymentPath,
65
+ 'Deployment readiness must use `- Ready when: `GET` `/path` returns `200``.',
66
+ line,
67
+ );
68
+ }
69
+ const status = Number(match[2]);
70
+ if (!Number.isInteger(status) || status < 200 || status > 399) {
71
+ invalid(deploymentPath, 'Deployment readiness status must be from 200 through 399.', line);
72
+ }
73
+ return {
74
+ kind: 'http',
75
+ method: 'GET',
76
+ path: parseUrlPath(match[1], deploymentPath, line),
77
+ status,
78
+ };
79
+ }
80
+
81
+ function parseStep(match, deploymentPath, line) {
82
+ const label = match[2].trim();
83
+ const argv = parseBacktickedArguments(match[3]);
84
+ if (!label || /[\0\r\n]/u.test(label)) {
85
+ invalid(deploymentPath, 'Deployment step labels must be non-empty single lines.', line);
86
+ }
87
+ if (!argv || argv.some((value) => value.includes('\0')) || !isSafeProcessExecutable(argv[0])) {
88
+ invalid(
89
+ deploymentPath,
90
+ 'Deployment command and arguments must be separate non-empty backticked values with a safe executable.',
91
+ line,
92
+ );
93
+ }
94
+ return {
95
+ label,
96
+ argv,
97
+ role: match[1].toLowerCase(),
98
+ };
99
+ }
100
+
101
+ export function parseStackDeploymentLines(lines, {
102
+ path: deploymentPath = 'stack deployment',
103
+ } = {}) {
104
+ if (lines === undefined) return null;
105
+ const entries = lines.map((line) => line.trim()).filter(Boolean);
106
+ if (entries.length === 1 && entries[0] === '- Nothing.') return { version: 1, steps: [] };
107
+
108
+ const result = {
109
+ version: 1,
110
+ workdir: '.',
111
+ runtimeRequirements: [],
112
+ readiness: null,
113
+ steps: [],
114
+ };
115
+ const seen = new Set();
116
+ for (let index = 0; index < lines.length; index += 1) {
117
+ const source = lines[index].trim();
118
+ if (!source) continue;
119
+ const line = index + 1;
120
+ if (source.startsWith('- Workdir: ')) {
121
+ if (seen.has('workdir')) invalid(deploymentPath, 'Duplicate Deployment Workdir.', line);
122
+ seen.add('workdir');
123
+ const workdir = oneToken(source, '- Workdir: ', deploymentPath, 'Deployment Workdir', line);
124
+ if (!isCanonicalProjectWorkdir(workdir)) {
125
+ invalid(deploymentPath, 'Deployment workdir must be a canonical project-relative directory.', line);
126
+ }
127
+ result.workdir = workdir;
128
+ continue;
129
+ }
130
+ if (source.startsWith('- Runtimes: ')) {
131
+ if (seen.has('runtimes')) invalid(deploymentPath, 'Duplicate Deployment Runtimes.', line);
132
+ seen.add('runtimes');
133
+ result.runtimeRequirements = parseRuntimes(source, deploymentPath, line);
134
+ continue;
135
+ }
136
+ if (source.startsWith('- Ready when: ')) {
137
+ if (seen.has('readiness')) invalid(deploymentPath, 'Duplicate Deployment Ready when.', line);
138
+ seen.add('readiness');
139
+ result.readiness = parseReadiness(source, deploymentPath, line);
140
+ continue;
141
+ }
142
+ const step = source.match(STEP_LINE);
143
+ if (step) {
144
+ result.steps.push(parseStep(step, deploymentPath, line));
145
+ continue;
146
+ }
147
+ invalid(deploymentPath, `Unknown Deployment entry: ${source}.`, line);
148
+ }
149
+ if (result.steps.length === 0) {
150
+ invalid(deploymentPath, '## Deployment needs at least one command or exactly `- Nothing.`.');
151
+ }
152
+ const serveSteps = result.steps.filter(({ role }) => role === 'serve');
153
+ if (serveSteps.length !== 1 || result.steps.at(-1).role !== 'serve') {
154
+ invalid(deploymentPath, 'Deployment needs exactly one final Serve step.');
155
+ }
156
+ for (let index = 1; index < result.steps.length; index += 1) {
157
+ if (STEP_ROLE_ORDER[result.steps[index].role] < STEP_ROLE_ORDER[result.steps[index - 1].role]) {
158
+ invalid(
159
+ deploymentPath,
160
+ 'Deployment steps must be ordered Prepare, Build, Migrate, then Serve.',
161
+ );
162
+ }
163
+ }
164
+ if (!result.readiness) {
165
+ invalid(deploymentPath, 'Deployment needs one Ready when entry.');
166
+ }
167
+ return result;
168
+ }
169
+
170
+ export function composeStackDeployment(components, projectDeployment) {
171
+ if (projectDeployment) return { ...projectDeployment, source: 'project', diagnostics: [] };
172
+ const declarations = components
173
+ .filter((component) => component.deployment?.steps?.length > 0)
174
+ .map((component) => ({
175
+ ...component.deployment,
176
+ source: `component:${component.id}`,
177
+ }));
178
+ if (declarations.length === 0) {
179
+ return {
180
+ version: 1,
181
+ workdir: '.',
182
+ runtimeRequirements: [],
183
+ readiness: null,
184
+ steps: [],
185
+ source: null,
186
+ diagnostics: [],
187
+ };
188
+ }
189
+ if (declarations.length === 1) return { ...declarations[0], diagnostics: [] };
190
+ const sources = declarations.map(({ source }) => source).sort();
191
+ return {
192
+ version: 1,
193
+ workdir: '.',
194
+ runtimeRequirements: [],
195
+ readiness: null,
196
+ steps: [],
197
+ source: null,
198
+ diagnostics: [{
199
+ code: 'STACK_DEPLOYMENT_AMBIGUOUS',
200
+ message: `Selected Stack components provide competing Deployment recipes: ${sources.join(', ')}. Add one project ## Deployment section to choose the exact recipe.`,
201
+ details: { sources },
202
+ }],
203
+ };
204
+ }
@@ -0,0 +1,79 @@
1
+ import { GenesisError } from './errors.js';
2
+
3
+ const DEFAULT_LINE = /^- Default `([A-Za-z_][A-Za-z0-9_]*)`: `([^`\r\n]+)`[ \t]*$/u;
4
+
5
+ function invalid(defaultsPath, message, line, details = {}) {
6
+ throw new GenesisError('STACK_ENVIRONMENT_DEFAULTS_INVALID', message, {
7
+ path: defaultsPath,
8
+ ...(line === undefined ? {} : { line }),
9
+ ...details,
10
+ });
11
+ }
12
+
13
+ export function parseStackEnvironmentDefaultLines(lines, {
14
+ path: defaultsPath = 'stack environment defaults',
15
+ } = {}) {
16
+ if (lines === undefined) return [];
17
+ const defaults = [];
18
+ for (let index = 0; index < lines.length; index += 1) {
19
+ const source = lines[index].trim();
20
+ if (!source) continue;
21
+ const entry = source.match(DEFAULT_LINE);
22
+ if (!entry) {
23
+ invalid(
24
+ defaultsPath,
25
+ 'Every Environment defaults entry must be a Default bullet with a backticked name and value.',
26
+ index + 1,
27
+ { observed: source },
28
+ );
29
+ }
30
+ defaults.push({ name: entry[1], value: entry[2] });
31
+ }
32
+ if (defaults.length === 0) {
33
+ invalid(defaultsPath, '## Environment defaults needs at least one Default entry.');
34
+ }
35
+ if (new Set(defaults.map(({ name }) => name)).size !== defaults.length) {
36
+ invalid(defaultsPath, '## Environment defaults contains a duplicate name.');
37
+ }
38
+ return defaults;
39
+ }
40
+
41
+ export function composeStackEnvironmentDefaults(components) {
42
+ const defaults = new Map();
43
+ for (const component of components) {
44
+ for (const declaration of component.environmentDefaults) {
45
+ const existing = defaults.get(declaration.name);
46
+ if (existing && existing.value !== declaration.value) {
47
+ invalid(
48
+ `component:${component.id}`,
49
+ `Stack components declare incompatible defaults for ${declaration.name}.`,
50
+ undefined,
51
+ {
52
+ name: declaration.name,
53
+ sources: [...existing.sources, `component:${component.id}`],
54
+ },
55
+ );
56
+ }
57
+ if (existing) {
58
+ existing.sources.push(`component:${component.id}`);
59
+ } else {
60
+ defaults.set(declaration.name, {
61
+ ...declaration,
62
+ sources: [`component:${component.id}`],
63
+ });
64
+ }
65
+ }
66
+ }
67
+ return [...defaults.values()].sort((left, right) => left.name.localeCompare(right.name));
68
+ }
69
+
70
+ /** Apply public Stack constants beneath explicit host or user environment. */
71
+ export function withStackEnvironmentDefaults(environment, defaults = []) {
72
+ if (!environment || typeof environment !== 'object' || Array.isArray(environment)) {
73
+ throw new TypeError('Stack environment resolution requires an environment object.');
74
+ }
75
+ return {
76
+ ...Object.fromEntries(defaults.map(({ name, value }) => [name, value])),
77
+ ...environment,
78
+ };
79
+ }
@@ -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,7 @@
1
1
  import { GenesisError } from './errors.js';
2
2
  import { parseStackCommandLines } from './stack-command.js';
3
+ import { parseStackDeploymentLines } from './stack-deployment.js';
4
+ import { parseStackEnvironmentDefaultLines } from './stack-environment-defaults.js';
3
5
  import { parseStackEnvironmentFileLines } from './stack-environment-files.js';
4
6
  import { parseStackLaunchLines } from './stack-launch.js';
5
7
  import { parseStackWorkspaceSetupLines } from './stack-workspace-setup.js';
@@ -218,6 +220,10 @@ export function parseStackPieceSource(value, {
218
220
  const deslop = textSection(all, 'Deslop');
219
221
  if (!description) invalid(piecePath, 'Stack piece needs Description.');
220
222
  const commands = parseStackCommandLines(all.get('Commands') || [], { path: piecePath });
223
+ const environmentDefaults = parseStackEnvironmentDefaultLines(
224
+ all.has('Environment defaults') ? all.get('Environment defaults') : undefined,
225
+ { path: piecePath },
226
+ );
221
227
  const environmentFiles = parseStackEnvironmentFileLines(
222
228
  all.has('Environment files') ? all.get('Environment files') : undefined,
223
229
  { path: piecePath },
@@ -225,6 +231,10 @@ export function parseStackPieceSource(value, {
225
231
  const launch = parseStackLaunchLines(all.has('Launch') ? all.get('Launch') : undefined, {
226
232
  path: piecePath,
227
233
  });
234
+ const deployment = parseStackDeploymentLines(
235
+ all.has('Deployment') ? all.get('Deployment') : undefined,
236
+ { path: piecePath },
237
+ );
228
238
  const workspaceSetup = parseStackWorkspaceSetupLines(
229
239
  all.has('Workspace setup') ? all.get('Workspace setup') : undefined,
230
240
  { path: piecePath },
@@ -239,8 +249,10 @@ export function parseStackPieceSource(value, {
239
249
  skill: skill(all, piecePath),
240
250
  resources: resources(all, piecePath),
241
251
  commands: commands.map(({ line: _line, ...command }) => command),
252
+ environmentDefaults,
242
253
  environmentFiles: environmentFiles || [],
243
254
  launchTargets: launch?.targets || [],
255
+ deployment,
244
256
  workspaceSetupSteps: workspaceSetup || [],
245
257
  deslop,
246
258
  };
@@ -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,11 @@ 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 { composeStackDeployment, parseStackDeploymentLines } from './stack-deployment.js';
8
9
  import { resolveStackPieces } from './stack-composition.js';
10
+ import {
11
+ composeStackEnvironmentDefaults,
12
+ } from './stack-environment-defaults.js';
9
13
  import {
10
14
  composeStackEnvironmentFiles,
11
15
  parseStackEnvironmentFileLines,
@@ -30,6 +34,7 @@ const STACK_SECTIONS = new Set([
30
34
  'Environment files',
31
35
  'Workspace setup',
32
36
  'Commands',
37
+ 'Deployment',
33
38
  'Launch',
34
39
  ]);
35
40
  const STACK_CUSTOMIZATION_ROOT = 'genesis/stack';
@@ -97,6 +102,7 @@ function withoutOuterBlankLines(lines = []) {
97
102
  function renderStack({
98
103
  commandLines = [],
99
104
  componentIds: componentIdsValue,
105
+ deploymentLines = null,
100
106
  environmentFileLines = null,
101
107
  launchLines = null,
102
108
  workspaceSetupLines = null,
@@ -114,6 +120,9 @@ function renderStack({
114
120
  : ['', '## Workspace setup', '', ...withoutOuterBlankLines(workspaceSetupLines)]),
115
121
  ...(commandLines.length > 0 ? ['', '## Commands', ...commandLines] : []),
116
122
  ...(launchLines === null ? [] : ['', '## Launch', '', ...withoutOuterBlankLines(launchLines)]),
123
+ ...(deploymentLines === null
124
+ ? []
125
+ : ['', '## Deployment', '', ...withoutOuterBlankLines(deploymentLines)]),
117
126
  '',
118
127
  ].join('\n');
119
128
  }
@@ -152,6 +161,11 @@ export async function addStackPieces({ pieces, projectRoot }) {
152
161
  );
153
162
  const launchLines = sections.has('Launch') ? sections.get('Launch') : null;
154
163
  parseStackLaunchLines(launchLines === null ? undefined : launchLines, { path: STACK_PATH });
164
+ const deploymentLines = sections.has('Deployment') ? sections.get('Deployment') : null;
165
+ parseStackDeploymentLines(
166
+ deploymentLines === null ? undefined : deploymentLines,
167
+ { path: STACK_PATH },
168
+ );
155
169
  const workspaceSetupLines = sections.has('Workspace setup')
156
170
  ? sections.get('Workspace setup')
157
171
  : null;
@@ -162,6 +176,7 @@ export async function addStackPieces({ pieces, projectRoot }) {
162
176
  const rendered = renderStack({
163
177
  commandLines,
164
178
  componentIds: selected,
179
+ deploymentLines,
165
180
  environmentFileLines,
166
181
  launchLines,
167
182
  workspaceSetupLines,
@@ -250,6 +265,7 @@ export async function readStack(projectRoot) {
250
265
  const componentCommands = components.flatMap((piece) => piece.commands);
251
266
  const commands = distinctCommands(projectCommands.length > 0 ? projectCommands : componentCommands);
252
267
  const resources = resourceDeclarations(components);
268
+ const environmentDefaults = composeStackEnvironmentDefaults(components);
253
269
  const projectEnvironmentFiles = parseStackEnvironmentFileLines(
254
270
  sections.has('Environment files') ? sections.get('Environment files') : undefined,
255
271
  { path: STACK_PATH },
@@ -260,6 +276,11 @@ export async function readStack(projectRoot) {
260
276
  { path: STACK_PATH },
261
277
  );
262
278
  const launchTargets = composeStackLaunchTargets(components, projectLaunch);
279
+ const projectDeployment = parseStackDeploymentLines(
280
+ sections.has('Deployment') ? sections.get('Deployment') : undefined,
281
+ { path: STACK_PATH },
282
+ );
283
+ const deployment = composeStackDeployment(components, projectDeployment);
263
284
  const projectWorkspaceSetup = parseStackWorkspaceSetupLines(
264
285
  sections.has('Workspace setup') ? sections.get('Workspace setup') : undefined,
265
286
  { path: STACK_PATH },
@@ -270,15 +291,19 @@ export async function readStack(projectRoot) {
270
291
  identityHash: sha256(stableJson({
271
292
  components: components.map(({ id }) => id),
272
293
  commands: commands.map(({ label, argv }) => ({ label, argv })),
294
+ environmentDefaults,
273
295
  environmentFiles,
274
296
  launchTargets,
297
+ deployment,
275
298
  resources,
276
299
  workspaceSetup,
277
300
  })),
278
301
  components,
279
302
  commands,
303
+ environmentDefaults,
280
304
  environmentFiles,
281
305
  launchTargets,
306
+ deployment,
282
307
  workspaceSetup,
283
308
  resources,
284
309
  guidance: composedProse(components, 'guidance'),
@@ -295,8 +320,10 @@ export function stackPromptContext(stack) {
295
320
  requires: piece.requires,
296
321
  })),
297
322
  verifyCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
323
+ environmentDefaults: stack.environmentDefaults,
298
324
  environmentFiles: stack.environmentFiles,
299
325
  launchTargets: stack.launchTargets,
326
+ deployment: stack.deployment,
300
327
  workspaceSetup: stack.workspaceSetup,
301
328
  };
302
329
  }
@@ -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
  });
@@ -5,16 +5,22 @@ import { gitContext } from './git.js';
5
5
  import { readStack } from './stack.js';
6
6
  import { sha256, stableJson, uniqueSorted } from './utils.js';
7
7
 
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);
8
+ export async function inspectWorkspaceSetupForStack({ projectRoot: root, stack }) {
14
9
  const diagnostics = [...stack.workspaceSetup.diagnostics];
10
+ const applicableSteps = [];
15
11
  if (diagnostics.length === 0) {
16
12
  const waitingFor = [];
17
13
  for (const step of stack.workspaceSetup.steps) {
14
+ if (step.runWhen) {
15
+ try {
16
+ await access(path.join(root, step.runWhen));
17
+ applicableSteps.push(step);
18
+ } catch (error) {
19
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
20
+ }
21
+ continue;
22
+ }
23
+ applicableSteps.push(step);
18
24
  if (!step.readyWhen) continue;
19
25
  try {
20
26
  await access(path.join(root, step.readyWhen));
@@ -36,9 +42,9 @@ export async function inspectProjectWorkspaceSetup({
36
42
  const blocked = diagnostics.some(({ code }) => code !== 'STACK_WORKSPACE_SETUP_WAITING');
37
43
  let status = 'unconfigured';
38
44
  if (blocked) status = 'blocked';
39
- else if (!waiting && stack.workspaceSetup.steps.length > 0) status = 'ready';
45
+ else if (!waiting && applicableSteps.length > 0) status = 'ready';
40
46
  const recipeHash = status === 'ready'
41
- ? sha256(stableJson({ version: 1, steps: stack.workspaceSetup.steps }))
47
+ ? sha256(stableJson({ version: 1, steps: applicableSteps }))
42
48
  : '';
43
49
  return {
44
50
  status,
@@ -47,9 +53,18 @@ export async function inspectProjectWorkspaceSetup({
47
53
  components: stack.components.map(({ id }) => id),
48
54
  source: stack.workspaceSetup.source,
49
55
  runtimeRequirements: uniqueSorted(
50
- stack.workspaceSetup.steps.flatMap((step) => step.runtimeRequirements),
56
+ applicableSteps.flatMap((step) => step.runtimeRequirements),
51
57
  ),
52
- steps: stack.workspaceSetup.steps,
58
+ steps: applicableSteps,
53
59
  diagnostics,
54
60
  };
55
61
  }
62
+
63
+ /** Read the Stack's workspace preparation recipe without executing it. */
64
+ export async function inspectProjectWorkspaceSetup({
65
+ projectRoot,
66
+ } = {}) {
67
+ const root = (await gitContext(projectRoot)).repositoryRoot;
68
+ const stack = await readStack(root);
69
+ return inspectWorkspaceSetupForStack({ projectRoot: root, stack });
70
+ }
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ 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';
@@ -77,6 +78,10 @@ export function inspectEnvironment(options) {
77
78
  return inspectProjectEnvironment(options);
78
79
  }
79
80
 
81
+ export function inspectDeployment(options) {
82
+ return inspectProjectDeployment(options);
83
+ }
84
+
80
85
  export function inspectWorkspaceSetup(options) {
81
86
  return inspectProjectWorkspaceSetup(options);
82
87
  }
@@ -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,6 +30,7 @@ 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
 
@@ -42,21 +43,30 @@ generated files, and runtime composition.
42
43
  - Default.
43
44
  - Preferred port: `3000`
44
45
  - URL path: `/`
46
+ - Ready when: `GET` `/api/health` returns `200`
45
47
  - Runtimes: `nodejs`
46
- - Prepare `Build`: `npm` `run` `build`
48
+ - Serve `Develop`: `npm` `run` `develop`
49
+
50
+ ## Deployment
51
+
52
+ - Runtimes: `nodejs`
53
+ - Ready when: `GET` `/api/health` returns `200`
54
+ - Prepare `Install dependencies`: `npm` `install`
55
+ - Build `Build`: `npm` `run` `build`
56
+ - Migrate `Prepare database`: `npm` `run` `db:prepare`
47
57
  - Serve `Start`: `npm` `start`
48
58
 
49
59
  ## Deslop
50
60
 
51
61
  - For every affected JSKIT screen, load the installed `jskit` Agent Skill,
52
62
  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
63
+ - Use JSKIT patterns, packages, resources, composables, screens, and placement
64
+ seams instead of parallel framework plumbing.
65
+ - Treat copied pattern source as app-owned and customizable. Adapt routes,
66
+ placements, and infrastructure tests coherently; when replacing a foundation
57
67
  route, update its baseline browser coverage instead of deleting it.
58
68
  - Consolidate repeated validation, pending/error state, API access, and page
59
- behavior at the generated shared resource or established JSKIT seam.
69
+ behavior at the shared resource or established JSKIT seam.
60
70
  - Replace transient command-error alerts that shift page content with the
61
71
  established JSKIT action feedback/snackbar seam; preserve stable in-page
62
72
  resource-load errors, retry states, and field validation.