create-substrat 0.4.1 → 0.5.0
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/index.js
CHANGED
|
@@ -20,13 +20,16 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
20
20
|
const TEMPLATE = join(HERE, 'template');
|
|
21
21
|
|
|
22
22
|
// Published today; Substrat is 0.x, so these are caret ranges on the current minor.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// The runtime packages release together off one version line, so one constant is right
|
|
24
|
+
// for all of them.
|
|
25
|
+
const SUBSTRAT = '^0.71.0';
|
|
26
|
+
// Engines do NOT share a line — each one versions on its own, so a single ENGINES
|
|
27
|
+
// constant silently stops resolving the moment any engine crosses a minor. It did:
|
|
28
|
+
// this file pinned `^0.3.37` while workorder had moved to 0.4.x and invoicing to 0.6.x,
|
|
29
|
+
// so a freshly scaffolded project could not install. One pin per engine, deliberately.
|
|
30
|
+
const ENGINE_WORKORDER = '^0.4.3';
|
|
31
|
+
const ENGINE_INVOICING = '^0.6.2';
|
|
32
|
+
const BOUNDARY_LINT = '^0.0.7';
|
|
30
33
|
|
|
31
34
|
const DOCS = 'https://substrat.net';
|
|
32
35
|
|
|
@@ -95,8 +98,8 @@ function packageJson(name) {
|
|
|
95
98
|
'@substrat-run/adapter-sqlite': SUBSTRAT,
|
|
96
99
|
'@substrat-run/adapter-cloudflare': SUBSTRAT,
|
|
97
100
|
'@substrat-run/vertical-host': SUBSTRAT,
|
|
98
|
-
'@substrat-run/engine-workorder':
|
|
99
|
-
'@substrat-run/engine-invoicing':
|
|
101
|
+
'@substrat-run/engine-workorder': ENGINE_WORKORDER,
|
|
102
|
+
'@substrat-run/engine-invoicing': ENGINE_INVOICING,
|
|
100
103
|
hono: '^4.6.0',
|
|
101
104
|
'@hono/node-server': '^1.13.0',
|
|
102
105
|
'better-sqlite3': '^13.0.3',
|
|
@@ -159,6 +162,12 @@ dist/
|
|
|
159
162
|
*.db
|
|
160
163
|
.data/
|
|
161
164
|
.DS_Store
|
|
165
|
+
|
|
166
|
+
# Which kernel version this checkout last announced to an agent (.substrat/hooks
|
|
167
|
+
# /session-start.mjs). Per-checkout state, not a project fact.
|
|
168
|
+
.substrat/.docs-pin
|
|
169
|
+
# Opt out of that hook entirely by creating this file.
|
|
170
|
+
.substrat/no-session-context
|
|
162
171
|
`;
|
|
163
172
|
|
|
164
173
|
function readme(name) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/.substrat/hooks/session-start.mjs\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook — the project announces itself (#754).
|
|
4
|
+
*
|
|
5
|
+
* The smooth part of a framework's agent integration is not the skills, it is
|
|
6
|
+
* that the user never has to remember the framework *has* an integration. This
|
|
7
|
+
* runs when an agent session starts, and if the project is a Substrat vertical
|
|
8
|
+
* it hands the agent three things it would otherwise have to discover: what this
|
|
9
|
+
* project is, where the rules live, and **which version of the docs describes the
|
|
10
|
+
* kernel actually installed here**.
|
|
11
|
+
*
|
|
12
|
+
* That last one is the reason this exists. Substrat is 0.x and interfaces change
|
|
13
|
+
* without notice, so an agent working from pages it cached two minors ago is the
|
|
14
|
+
* expensive failure — confident, plausible, and wrong. `llms.txt` is published at
|
|
15
|
+
* a version-pinned URL precisely so this hook can point at the matching slice.
|
|
16
|
+
*
|
|
17
|
+
* ## Deliberately not tool-specific
|
|
18
|
+
*
|
|
19
|
+
* This file lives in `.substrat/` — the tool-neutral home, next to `playbook.md` —
|
|
20
|
+
* not in `.claude/`. `.claude/settings.json` is a three-line adapter that runs it,
|
|
21
|
+
* and any other client that grows a session hook binds the same way. It also
|
|
22
|
+
* means the plugin distribution (#753) can ship this script unchanged rather than
|
|
23
|
+
* forking it.
|
|
24
|
+
*
|
|
25
|
+
* ## Deliberately silent, and deliberately offline
|
|
26
|
+
*
|
|
27
|
+
* It prints nothing at all unless `package.json` has a `substrat` block, so it is
|
|
28
|
+
* inert in any other project. And it makes **no network request**: session start
|
|
29
|
+
* is on the critical path of every session and is frequently offline, while the
|
|
30
|
+
* check a fetch would perform — does the published doc set still describe my
|
|
31
|
+
* kernel — is already mechanical for whoever actually fetches, because
|
|
32
|
+
* `/llms-<version>.txt` 404s exactly when the answer is no.
|
|
33
|
+
*
|
|
34
|
+
* Opt out by creating `.substrat/no-session-context`.
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
37
|
+
import { dirname, join } from 'node:path';
|
|
38
|
+
|
|
39
|
+
const DOCS = 'https://substrat.net';
|
|
40
|
+
const KERNEL = '@substrat-run/kernel';
|
|
41
|
+
|
|
42
|
+
/** Where the project is. Claude Code sets this; fall back to the cwd it ran us in. */
|
|
43
|
+
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
44
|
+
|
|
45
|
+
/** The marker: which kernel version this project last announced. */
|
|
46
|
+
const MARKER = join(root, '.substrat', '.docs-pin');
|
|
47
|
+
const OPT_OUT = join(root, '.substrat', 'no-session-context');
|
|
48
|
+
|
|
49
|
+
/** Nothing this hook does is worth failing a session over. */
|
|
50
|
+
function silent() {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readJson(path) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
if (existsSync(OPT_OUT)) silent();
|
|
64
|
+
|
|
65
|
+
const pkg = readJson(join(root, 'package.json'));
|
|
66
|
+
// The detection signal is the block `substrat push` already reads — no sentinel
|
|
67
|
+
// file to invent, and nothing to keep in sync.
|
|
68
|
+
if (!pkg?.substrat) silent();
|
|
69
|
+
|
|
70
|
+
if (existsSync(OPT_OUT)) silent();
|
|
71
|
+
|
|
72
|
+
// What is actually installed beats what package.json asked for: a caret range on
|
|
73
|
+
// 0.x pins the minor, and the resolved version is what the code compiles against.
|
|
74
|
+
const installed = readJson(join(root, 'node_modules', KERNEL, 'package.json'))?.version;
|
|
75
|
+
const declared = pkg.dependencies?.[KERNEL] ?? pkg.devDependencies?.[KERNEL];
|
|
76
|
+
|
|
77
|
+
const lines = [
|
|
78
|
+
'This project is a **Substrat vertical** — a multi-tenant business app on the',
|
|
79
|
+
'Substrat kernel and its engines.',
|
|
80
|
+
'',
|
|
81
|
+
'- The always-on rules you must not violate are in `AGENTS.md` — module-code',
|
|
82
|
+
' boundaries, the gates, and the two checkpoints you may never self-approve.',
|
|
83
|
+
'- The build flow (interview → design → reshape → checkpoints) is `.substrat/playbook.md`.',
|
|
84
|
+
' It is a playbook, not always-on context: read it when starting or extending a vertical.',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
if (installed) {
|
|
88
|
+
lines.push(
|
|
89
|
+
'',
|
|
90
|
+
`This project has **${KERNEL} ${installed}** installed.`,
|
|
91
|
+
'',
|
|
92
|
+
`Docs describing that exact version: ${DOCS}/llms-${installed}.txt`,
|
|
93
|
+
'',
|
|
94
|
+
'That URL returns 200 only while the published docs still describe this kernel. A 404',
|
|
95
|
+
`means they have moved on — fetch ${DOCS}/llms.txt instead, and treat anything you`,
|
|
96
|
+
'already believe about the API as unverified. Substrat is pre-1.0 and interfaces change',
|
|
97
|
+
'without notice, so do not answer from memory about its surface; the pages are markdown',
|
|
98
|
+
'at those URLs and every doc page has a `.md` twin.',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const announced = existsSync(MARKER) ? readFileSync(MARKER, 'utf8').trim() : '';
|
|
102
|
+
if (announced && announced !== installed) {
|
|
103
|
+
lines.unshift(
|
|
104
|
+
`**The kernel moved: ${announced} → ${installed} since the last session here.**`,
|
|
105
|
+
'Re-read the docs slice below before relying on anything you remember about the API.',
|
|
106
|
+
'',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
mkdirSync(dirname(MARKER), { recursive: true });
|
|
111
|
+
writeFileSync(MARKER, `${installed}\n`);
|
|
112
|
+
} catch {
|
|
113
|
+
// A read-only checkout still gets the context; it just re-announces next time.
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(
|
|
117
|
+
'',
|
|
118
|
+
`**${KERNEL} is not installed yet** (\`package.json\` asks for \`${declared ?? 'it'}\`).`,
|
|
119
|
+
'Run the install before trusting any version-specific guidance, then re-check the docs',
|
|
120
|
+
`index at ${DOCS}/llms.txt.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
process.stdout.write(
|
|
125
|
+
`${JSON.stringify({
|
|
126
|
+
hookSpecificOutput: {
|
|
127
|
+
hookEventName: 'SessionStart',
|
|
128
|
+
additionalContext: lines.join('\n'),
|
|
129
|
+
},
|
|
130
|
+
})}\n`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
main();
|
|
136
|
+
} catch {
|
|
137
|
+
// Never break a session because orientation failed.
|
|
138
|
+
silent();
|
|
139
|
+
}
|
|
@@ -222,7 +222,62 @@ Approval of the design is what unlocks Step 5. Until you have it, you are still
|
|
|
222
222
|
|
|
223
223
|
---
|
|
224
224
|
|
|
225
|
-
## Step 5 —
|
|
225
|
+
## Step 5 — Declare the model
|
|
226
|
+
|
|
227
|
+
The design is approved. Before reshaping any code, declare **what exists** in
|
|
228
|
+
`spec/model.ts`: entities, the operations over them, and the permissions those operations
|
|
229
|
+
check. One TypeScript module, and the compiler checks the joins between them.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { defineEntities, defineOperations, emitModel } from '@substrat-run/contracts';
|
|
233
|
+
import { z } from '@substrat-run/contracts';
|
|
234
|
+
|
|
235
|
+
export const entities = defineEntities({
|
|
236
|
+
customer: {
|
|
237
|
+
table: 'acme_customers',
|
|
238
|
+
fields: z.object({ id: z.string(), number: z.string(), name: z.string() }),
|
|
239
|
+
key: ['number'],
|
|
240
|
+
erasable: ['name'],
|
|
241
|
+
},
|
|
242
|
+
site: { table: 'acme_sites', fields: z.object({ id: z.string(), customer_id: z.string() }), parents: ['customer'] },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
export const PERMISSIONS = ['customer:manage'] as const;
|
|
246
|
+
|
|
247
|
+
export const operations = defineOperations(entities, PERMISSIONS)({
|
|
248
|
+
'acme/create-customer': {
|
|
249
|
+
summary: 'Register a customer',
|
|
250
|
+
permission: 'customer:manage',
|
|
251
|
+
input: z.object({ number: z.string(), name: z.string() }),
|
|
252
|
+
output: entities.customer.fields,
|
|
253
|
+
emits: { entity: 'customer', entityIdFrom: 'id', type: 'acme.customer-created', schemaVersion: 1, piiClass: 'none' },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export const model = emitModel(entities);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
These are compile errors, not lints: a `parents` naming no entity, a `permission` that is
|
|
261
|
+
not declared, an `entityIdFrom` naming no field of that operation's `output`, a `payload`
|
|
262
|
+
carrying a field the entity marks `erasable`, a `{var}` in an HTTP path that names no input
|
|
263
|
+
field. All before a handler exists.
|
|
264
|
+
|
|
265
|
+
Field names mirror the SQL columns, snake_case included — a prettier naming here is a second
|
|
266
|
+
description of the same rows. Not every table is an entity: an entity is something the
|
|
267
|
+
platform can point at (attachments hang off one, grants narrow to one, events are about one).
|
|
268
|
+
|
|
269
|
+
Behaviour stays prose in `DESIGN.md`. Inventing a way to declare a state *transition* means
|
|
270
|
+
the boundary slipped.
|
|
271
|
+
|
|
272
|
+
Full reference: https://substrat.net/concepts/model
|
|
273
|
+
|
|
274
|
+
**Do not edit `spec/model.ts` while reshaping the code.** If a handler cannot return what the
|
|
275
|
+
model declares, that is real information — say so and stop, rather than reshaping the model
|
|
276
|
+
to make the build pass.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Step 6 — Reshape the reference
|
|
226
281
|
|
|
227
282
|
The design is approved. The scaffold already contains a working vertical in `src/` + `test/` —
|
|
228
283
|
the bike-repair shop. **Read it first** (it's your Callout: the real, green implementation of
|
|
@@ -314,7 +369,7 @@ closed-door assertion with a control proving a neighbouring door is still open.
|
|
|
314
369
|
|
|
315
370
|
---
|
|
316
371
|
|
|
317
|
-
## Step
|
|
372
|
+
## Step 7 — Run it
|
|
318
373
|
|
|
319
374
|
Build confidence in this order, and **show the user the output of each**:
|
|
320
375
|
|
|
@@ -336,7 +391,7 @@ and typed wrappers over the routes. Ask first — it roughly doubles the work.
|
|
|
336
391
|
|
|
337
392
|
---
|
|
338
393
|
|
|
339
|
-
## Step
|
|
394
|
+
## Step 8 — The two checkpoints. STOP HERE.
|
|
340
395
|
|
|
341
396
|
**You may never self-approve these. Present them and wait.** The design gate (Step 4) already
|
|
342
397
|
took the user's approval of *what* to build; these confirm that the code matches it.
|
|
@@ -357,7 +412,7 @@ and who can see other tenants' data?* A permission diff nobody understands is th
|
|
|
357
412
|
|
|
358
413
|
---
|
|
359
414
|
|
|
360
|
-
## Step
|
|
415
|
+
## Step 9 — Deploy (optional)
|
|
361
416
|
|
|
362
417
|
Only if the user asks. Local-first is a legitimate stopping point.
|
|
363
418
|
|
|
@@ -389,7 +444,7 @@ cross-tenant hole with a UI.
|
|
|
389
444
|
|
|
390
445
|
---
|
|
391
446
|
|
|
392
|
-
## Step
|
|
447
|
+
## Step 10 — Leave the project competent
|
|
393
448
|
|
|
394
449
|
The next session — in any tool — starts cold. The scaffold already ships `AGENTS.md`,
|
|
395
450
|
`CLAUDE.md`, and the Cursor/opencode command stubs, so the rules and this flow survive. Your
|