create-substrat 0.4.2 → 0.6.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/dev-servers.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The dev servers a scaffolded vertical runs — declared once, client-neutrally.
3
+ *
4
+ * `.claude/launch.json` is a Claude Desktop **adapter**: it lets the agent start
5
+ * the server, open it in the Browser pane, and verify its own changes. Under
6
+ * design/agent-surface.md §3 an adapter may route, describe and trigger but may
7
+ * never *hold* substance — so the topology lives here, in the `substrat` block of
8
+ * package.json that `substrat push` and the SessionStart hook already read, and
9
+ * the client file is emitted from it (`pnpm lint:launch`, guarded in CI).
10
+ *
11
+ * A port is deliberately NOT a field. `portEnv` + `portFrom` say *where* the port
12
+ * is bound; the number is read out of that file. Otherwise the declaration becomes
13
+ * a second copy of a number whose first copy is the one that actually runs, and
14
+ * with `autoPort: false` a stale copy is a hard boot failure — for an agent, mid
15
+ * session, which is the worst possible audience for it.
16
+ *
17
+ * @typedef {object} DevServer
18
+ * @property {string} name Entry name. Mirrors the `-n` label of the `dev` script.
19
+ * @property {string} run pnpm script to run. With `dir`, the script in that subdir.
20
+ * @property {string} [dir] Subdirectory of the project (a Vite app), if any.
21
+ * @property {string} portEnv The env var that moves this port (`PORT`, `WEB_PORT`, …).
22
+ * @property {string} portFrom Project-relative file binding it: `process.env.<portEnv> ?? N`.
23
+ * @property {Record<string,string>} [env] Env the `dev` script sets for this process.
24
+ */
25
+
26
+ /** The template ships one process: the Hono API. There is no web app to scaffold yet. */
27
+ export const DEV_SERVERS = [
28
+ { name: 'api', run: 'server', portEnv: 'PORT', portFrom: 'src/server.ts' },
29
+ ];
package/index.js CHANGED
@@ -16,6 +16,8 @@ import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:
16
16
  import { basename, dirname, join, resolve } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
18
 
19
+ import { DEV_SERVERS } from './dev-servers.js';
20
+
19
21
  const HERE = dirname(fileURLToPath(import.meta.url));
20
22
  const TEMPLATE = join(HERE, 'template');
21
23
 
@@ -73,7 +75,9 @@ function packageJson(name) {
73
75
  // What `substrat push` reads: the permission surface (the registry the
74
76
  // promotion checkpoint diffs) and the runtime needs the deploy config is
75
77
  // derived from — you never author wrangler config (src/worker.ts is the
76
- // entry; ScopeDO is the store it exports).
78
+ // entry; ScopeDO is the store it exports). Also the client-neutral home of
79
+ // the dev topology `.claude/launch.json` is emitted from — see
80
+ // dev-servers.js for why the port is read from the code and never declared.
77
81
  substrat: {
78
82
  permissions: 'src/provision.ts',
79
83
  runtimeNeeds: {
@@ -84,6 +88,7 @@ function packageJson(name) {
84
88
  { binding: 'SWEEPER', class: 'SweeperDO' },
85
89
  ],
86
90
  },
91
+ devServers: DEV_SERVERS,
87
92
  },
88
93
  scripts: {
89
94
  dev: 'tsx watch src/server.ts',
@@ -162,6 +167,12 @@ dist/
162
167
  *.db
163
168
  .data/
164
169
  .DS_Store
170
+
171
+ # Which kernel version this checkout last announced to an agent (.substrat/hooks
172
+ # /session-start.mjs). Per-checkout state, not a project fact.
173
+ .substrat/.docs-pin
174
+ # Opt out of that hook entirely by creating this file.
175
+ .substrat/no-session-context
165
176
  `;
166
177
 
167
178
  function readme(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-substrat",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "index.js",
27
+ "dev-servers.js",
27
28
  "template"
28
29
  ],
29
30
  "engines": {
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "api",
6
+ "runtimeExecutable": "pnpm",
7
+ "runtimeArgs": [
8
+ "run",
9
+ "server"
10
+ ],
11
+ "port": 8873,
12
+ "autoPort": false
13
+ }
14
+ ]
15
+ }
@@ -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
+ }