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 +42 -13
- package/docs/stack-components.md +69 -14
- package/package.json +1 -1
- 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/index/check.js +5 -1
- package/src/index/deployment.js +31 -0
- package/src/index/environment-files.js +10 -3
- package/src/index/launch.js +6 -1
- package/src/index/prompt.js +5 -1
- package/src/index/stack-catalog.js +1 -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 +12 -0
- package/src/index/stack-workspace-setup.js +7 -3
- package/src/index/stack.js +27 -0
- package/src/index/verification.js +31 -1
- package/src/index/workspace-setup.js +25 -10
- package/src/index.js +5 -0
- package/stacks/pieces/jskit-mysql.md +5 -1
- package/stacks/pieces/jskit-postgresql.md +5 -1
- package/stacks/pieces/jskit.md +19 -9
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
|
|
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,
|
|
138
|
-
diagnostics, and paths. It never returns
|
|
139
|
-
|
|
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
|
|
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
|
|
150
|
-
execution and secrets, browser binaries, Git, and
|
|
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
|
|
336
|
-
|
|
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
|
|
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:
|
package/docs/stack-components.md
CHANGED
|
@@ -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. `
|
|
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
|
|
149
|
-
|
|
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,
|
|
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
|
|
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
|
|
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`,
|
|
264
|
-
`diagnostics`, and normalized `files`. Each
|
|
265
|
-
`
|
|
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`,
|
|
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.
|
|
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
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
package/src/index/check.js
CHANGED
|
@@ -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({
|
|
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
|
|
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({
|
|
10
|
-
|
|
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,
|
package/src/index/launch.js
CHANGED
|
@@ -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({
|
|
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,
|
package/src/index/prompt.js
CHANGED
|
@@ -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({
|
|
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
|
package/src/index/stack-piece.js
CHANGED
|
@@ -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
|
|
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,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
|
|
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
|
-
|
|
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 &&
|
|
45
|
+
else if (!waiting && applicableSteps.length > 0) status = 'ready';
|
|
40
46
|
const recipeHash = status === 'ready'
|
|
41
|
-
? sha256(stableJson({ version: 1, 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
|
-
|
|
56
|
+
applicableSteps.flatMap((step) => step.runtimeRequirements),
|
|
51
57
|
),
|
|
52
|
-
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": ["
|
|
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,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
|
-
-
|
|
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
|
|
54
|
-
|
|
55
|
-
- Treat
|
|
56
|
-
placements, and infrastructure tests coherently; when replacing a
|
|
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
|
|
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.
|