create-substrat 0.0.1 → 0.1.1
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 +214 -33
- package/package.json +4 -3
- package/template/.claude/skills/substrat/SKILL.md +13 -0
- package/template/.cursor/commands/new-vertical.md +7 -0
- package/template/.cursor/rules/substrat.mdc +11 -0
- package/template/.opencode/command/new-vertical.md +9 -0
- package/template/.substrat/playbook.md +386 -0
- package/template/AGENTS.md +117 -0
- package/template/CLAUDE.md +8 -0
- package/template/src/manifest.ts +48 -0
- package/template/src/migrations.ts +39 -0
- package/template/src/module.ts +297 -0
- package/template/src/seed.ts +265 -0
- package/template/src/server.ts +145 -0
- package/template/test/scenario.test.ts +249 -0
package/index.js
CHANGED
|
@@ -1,39 +1,220 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `npm create substrat
|
|
3
|
+
* `npm create substrat <dir>` — scaffold a Substrat vertical.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Copies the instruction layer (AGENTS.md, the playbook, and the per-tool command
|
|
6
|
+
* stubs for Claude Code / Cursor / opencode) into a new project, then generates the
|
|
7
|
+
* tooling configs that need the project name interpolated. The result is a project
|
|
8
|
+
* that installs and whose AI tools already know the rules and the build flow — the
|
|
9
|
+
* agent writes the vertical itself, guided by `.substrat/playbook.md`.
|
|
8
10
|
*
|
|
9
|
-
* Dependency-free and buildless on purpose —
|
|
10
|
-
* install time
|
|
11
|
+
* Dependency-free and buildless on purpose — node built-ins only, so it can never
|
|
12
|
+
* break at install time and damage the name it exists to protect.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const TEMPLATE = join(HERE, 'template');
|
|
21
|
+
|
|
22
|
+
// Published today; Substrat is 0.x, so these are caret ranges on the current minor.
|
|
23
|
+
const SUBSTRAT = '^0.29.0';
|
|
24
|
+
// Engines version on their own line (0.3.x), independent of the kernel/contracts line.
|
|
25
|
+
const ENGINES = '^0.3.27';
|
|
26
|
+
const BOUNDARY_LINT = '^0.0.5';
|
|
27
|
+
|
|
28
|
+
const DOCS = 'https://substrat.net';
|
|
29
|
+
|
|
30
|
+
function fail(message) {
|
|
31
|
+
process.stderr.write(`\n create-substrat: ${message}\n\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
process.stdout.write(
|
|
37
|
+
[
|
|
38
|
+
'',
|
|
39
|
+
' Scaffold a Substrat vertical.',
|
|
40
|
+
'',
|
|
41
|
+
' npm create substrat <dir>',
|
|
42
|
+
' npm create substrat . # scaffold into the current directory',
|
|
43
|
+
'',
|
|
44
|
+
` Docs: ${DOCS}`,
|
|
45
|
+
'',
|
|
46
|
+
'',
|
|
47
|
+
].join('\n'),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** npm names: lowercase, url-safe, no leading dot/underscore. */
|
|
52
|
+
function toPackageName(dir) {
|
|
53
|
+
const name = basename(resolve(dir))
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9-~]+/g, '-')
|
|
56
|
+
.replace(/^[-_.]+|[-_.]+$/g, '');
|
|
57
|
+
return name || 'substrat-vertical';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function packageJson(name) {
|
|
61
|
+
return `${JSON.stringify(
|
|
62
|
+
{
|
|
63
|
+
name,
|
|
64
|
+
version: '0.0.0',
|
|
65
|
+
private: true,
|
|
66
|
+
type: 'module',
|
|
67
|
+
scripts: {
|
|
68
|
+
dev: 'tsx watch src/server.ts',
|
|
69
|
+
server: 'tsx src/server.ts',
|
|
70
|
+
test: 'vitest run',
|
|
71
|
+
typecheck: 'tsc --noEmit',
|
|
72
|
+
'lint:boundaries': 'substrat-boundary-lint',
|
|
73
|
+
},
|
|
74
|
+
dependencies: {
|
|
75
|
+
'@substrat-run/kernel': SUBSTRAT,
|
|
76
|
+
'@substrat-run/contracts': SUBSTRAT,
|
|
77
|
+
'@substrat-run/adapter-sqlite': SUBSTRAT,
|
|
78
|
+
'@substrat-run/engine-workorder': ENGINES,
|
|
79
|
+
'@substrat-run/engine-invoicing': ENGINES,
|
|
80
|
+
hono: '^4.6.0',
|
|
81
|
+
'@hono/node-server': '^1.13.0',
|
|
82
|
+
'better-sqlite3': '^12.0.0',
|
|
83
|
+
},
|
|
84
|
+
devDependencies: {
|
|
85
|
+
'@substrat-run/boundary-lint': BOUNDARY_LINT,
|
|
86
|
+
'@types/better-sqlite3': '^7.6.0',
|
|
87
|
+
concurrently: '^9.0.0',
|
|
88
|
+
tsx: '^4.19.0',
|
|
89
|
+
typescript: '^5.6.0',
|
|
90
|
+
vitest: '^3.0.0',
|
|
91
|
+
},
|
|
92
|
+
// Do NOT add `zod` here — import `z` from `@substrat-run/contracts` (AGENTS.md, rule 10).
|
|
93
|
+
pnpm: { onlyBuiltDependencies: ['better-sqlite3'] },
|
|
94
|
+
},
|
|
95
|
+
null,
|
|
96
|
+
2,
|
|
97
|
+
)}\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const TSCONFIG = `${JSON.stringify(
|
|
101
|
+
{
|
|
102
|
+
compilerOptions: {
|
|
103
|
+
target: 'ES2022',
|
|
104
|
+
module: 'NodeNext',
|
|
105
|
+
moduleResolution: 'NodeNext',
|
|
106
|
+
lib: ['ES2022'],
|
|
107
|
+
strict: true,
|
|
108
|
+
esModuleInterop: true,
|
|
109
|
+
skipLibCheck: true,
|
|
110
|
+
forceConsistentCasingInFileNames: true,
|
|
111
|
+
noEmit: true,
|
|
112
|
+
types: ['node'],
|
|
113
|
+
},
|
|
114
|
+
include: ['src', 'test'],
|
|
115
|
+
},
|
|
116
|
+
null,
|
|
117
|
+
2,
|
|
118
|
+
)}\n`;
|
|
119
|
+
|
|
120
|
+
const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
121
|
+
|
|
122
|
+
export default defineConfig({
|
|
123
|
+
test: {
|
|
124
|
+
include: ['test/**/*.test.ts'],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const GITIGNORE = `node_modules/
|
|
130
|
+
dist/
|
|
131
|
+
*.sqlite
|
|
132
|
+
*.sqlite-*
|
|
133
|
+
*.db
|
|
134
|
+
.data/
|
|
135
|
+
.DS_Store
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
function readme(name) {
|
|
139
|
+
return `# ${name}
|
|
140
|
+
|
|
141
|
+
A multi-tenant business app built on [Substrat](${DOCS}).
|
|
142
|
+
|
|
143
|
+
\`src/\` ships with a small **working reference vertical** (a bike-repair shop, green via
|
|
144
|
+
\`npm test\`) — your worked example and starting point. The build flow reshapes it into your
|
|
145
|
+
own domain; it is not meant to survive as-is.
|
|
146
|
+
|
|
147
|
+
## Build it
|
|
148
|
+
|
|
149
|
+
Open this project in Claude Code, Cursor, or opencode and start the build flow:
|
|
150
|
+
|
|
151
|
+
- **Claude Code**: \`/substrat\`
|
|
152
|
+
- **Cursor / opencode**: run the \`new-vertical\` command
|
|
153
|
+
|
|
154
|
+
Both follow [\`.substrat/playbook.md\`](.substrat/playbook.md). The always-on rules the
|
|
155
|
+
agent must not violate live in [\`AGENTS.md\`](AGENTS.md).
|
|
156
|
+
|
|
157
|
+
## Gates
|
|
158
|
+
|
|
159
|
+
\`\`\`sh
|
|
160
|
+
pnpm install
|
|
161
|
+
pnpm test # the scenario, including the denials
|
|
162
|
+
pnpm lint:boundaries # the layer rules (also: npx @substrat-run/boundary-lint)
|
|
163
|
+
pnpm typecheck
|
|
164
|
+
\`\`\`
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function main() {
|
|
169
|
+
const target = process.argv[2];
|
|
170
|
+
if (target === '-h' || target === '--help') {
|
|
171
|
+
usage();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
if (!target) {
|
|
175
|
+
usage();
|
|
176
|
+
fail('a target directory is required — e.g. `npm create substrat my-app` (or `.` for here).');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dest = resolve(target);
|
|
180
|
+
if (existsSync(dest) && readdirSync(dest).some((f) => f === 'package.json')) {
|
|
181
|
+
fail(`${target} already looks like a project (package.json present). Refusing to overwrite.`);
|
|
182
|
+
}
|
|
183
|
+
if (!existsSync(TEMPLATE)) {
|
|
184
|
+
fail('template payload is missing from the package — please report this.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
mkdirSync(dest, { recursive: true });
|
|
188
|
+
|
|
189
|
+
// The static instruction layer: AGENTS.md, CLAUDE.md, .substrat/, and the per-tool stubs.
|
|
190
|
+
cpSync(TEMPLATE, dest, { recursive: true });
|
|
191
|
+
|
|
192
|
+
// Generated configs — these need the project name, so they aren't in the template.
|
|
193
|
+
const name = toPackageName(target);
|
|
194
|
+
writeFileSync(join(dest, 'package.json'), packageJson(name));
|
|
195
|
+
writeFileSync(join(dest, 'tsconfig.json'), TSCONFIG);
|
|
196
|
+
writeFileSync(join(dest, 'vitest.config.ts'), VITEST_CONFIG);
|
|
197
|
+
writeFileSync(join(dest, '.gitignore'), GITIGNORE);
|
|
198
|
+
writeFileSync(join(dest, 'README.md'), readme(name));
|
|
199
|
+
|
|
200
|
+
const where = target === '.' ? '' : ` cd ${target}\n`;
|
|
201
|
+
process.stdout.write(
|
|
202
|
+
[
|
|
203
|
+
'',
|
|
204
|
+
` Scaffolded ${name}.`,
|
|
205
|
+
'',
|
|
206
|
+
' The instruction layer is in place — Claude Code, Cursor, and opencode all',
|
|
207
|
+
' read the same rules (AGENTS.md) and build flow (.substrat/playbook.md).',
|
|
208
|
+
'',
|
|
209
|
+
' Next:',
|
|
210
|
+
where + ' pnpm install',
|
|
211
|
+
' Then open the project in your AI editor and start the build flow:',
|
|
212
|
+
' Claude Code: /substrat',
|
|
213
|
+
' Cursor / opencode: the new-vertical command',
|
|
214
|
+
'',
|
|
215
|
+
'',
|
|
216
|
+
].join('\n'),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-substrat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a Substrat vertical — `npm create substrat
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"create-substrat": "./index.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"index.js"
|
|
26
|
+
"index.js",
|
|
27
|
+
"template"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=20"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: substrat
|
|
3
|
+
description: Build or extend a vertical on Substrat — interview, map the domain onto the engines, scaffold, run, and present the two human checkpoints. Use when asked to build, scaffold, or extend a multi-tenant business app / vertical / internal tool where tenancy, permissions, audit, or work-order-shaped workflows matter.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a vertical on Substrat
|
|
7
|
+
|
|
8
|
+
Read and follow [`.substrat/playbook.md`](../../../.substrat/playbook.md) in the project
|
|
9
|
+
root — the full flow (interview → coverage map → scaffold → run → checkpoints) lives there
|
|
10
|
+
so every tool reads one source of truth. The always-on rules are in
|
|
11
|
+
[`AGENTS.md`](../../../AGENTS.md); this skill is the invokable *flow* on top of them.
|
|
12
|
+
|
|
13
|
+
Do not skip the two human checkpoints in the playbook. You may never self-approve them.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Build a Substrat vertical
|
|
2
|
+
|
|
3
|
+
Read and follow [`.substrat/playbook.md`](../../.substrat/playbook.md) — the full flow to
|
|
4
|
+
interview the user, map their domain onto the engines, scaffold, run, and present the two
|
|
5
|
+
human checkpoints. The always-on rules are in [`AGENTS.md`](../../AGENTS.md).
|
|
6
|
+
|
|
7
|
+
Do not skip the two human checkpoints. Never self-approve them.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: How to build or extend this Substrat vertical — the interview → scaffold → run → checkpoints flow. Fetch when starting a new vertical, adding an operation, engine, migration, or permission, or wiring the server.
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Follow the full playbook in [`.substrat/playbook.md`](/.substrat/playbook.md) before
|
|
7
|
+
scaffolding or extending the vertical. The always-on rules (module-code boundaries, the
|
|
8
|
+
gates, the two human checkpoints) live in [`AGENTS.md`](/AGENTS.md) and are always in
|
|
9
|
+
context — this rule is the on-demand *flow* on top of them.
|
|
10
|
+
|
|
11
|
+
Never self-approve the migration diff or the permission diff.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build or extend a vertical on Substrat — interview, scaffold, run, checkpoints.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Read and follow `.substrat/playbook.md` — the full flow to interview the user, map their
|
|
6
|
+
domain onto the engines, scaffold, run, and present the two human checkpoints. The
|
|
7
|
+
always-on rules are in `AGENTS.md`.
|
|
8
|
+
|
|
9
|
+
Do not skip the two human checkpoints. Never self-approve them.
|