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 CHANGED
@@ -1,39 +1,220 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `npm create substrat` — placeholder.
3
+ * `npm create substrat <dir>` — scaffold a Substrat vertical.
4
4
  *
5
- * This reserves the entry point a user will guess. It deliberately does nothing
6
- * but point at what does exist: publishing a stub that pretends to scaffold
7
- * would be worse than publishing nothing.
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 — a placeholder that can break at
10
- * install time is a placeholder that damages the name it exists to protect.
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
- const DOCS = 'https://substrat.ahlstrand.es';
14
- const GUIDE = `${DOCS}/guide/getting-started`;
15
- const REPO = 'https://github.com/substrat-run/substrat';
16
-
17
- process.stdout.write(
18
- [
19
- '',
20
- ' Substrat — a hosted substrate for vertical business software.',
21
- '',
22
- ' The initializer is not released yet. This package reserves',
23
- ' `npm create substrat` for it.',
24
- '',
25
- ' To start a vertical today, follow the guide:',
26
- ` ${GUIDE}`,
27
- '',
28
- ' The packages are published and usable now:',
29
- ' pnpm add @substrat-run/kernel @substrat-run/contracts @substrat-run/adapter-sqlite zod',
30
- '',
31
- ` Docs: ${DOCS}`,
32
- ` Repo: ${REPO}`,
33
- '',
34
- ' Substrat is 0.x and interfaces change without notice until the first',
35
- ' vertical ships.',
36
- '',
37
- '',
38
- ].join('\n'),
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.0.1",
4
- "description": "Scaffold a Substrat vertical — `npm create substrat`. Placeholder reserving the entry point; the initializer is not released yet.",
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.