create-substrat 0.6.0 → 0.6.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/package.json
CHANGED
|
@@ -19,8 +19,13 @@
|
|
|
19
19
|
* This file lives in `.substrat/` — the tool-neutral home, next to `playbook.md` —
|
|
20
20
|
* not in `.claude/`. `.claude/settings.json` is a three-line adapter that runs it,
|
|
21
21
|
* and any other client that grows a session hook binds the same way. It also
|
|
22
|
-
* means the plugin distribution (#753)
|
|
23
|
-
* forking it.
|
|
22
|
+
* means the plugin distribution (#753) ships this script unchanged rather than
|
|
23
|
+
* forking it: `plugin/substrat/scripts/session-start.mjs` is emitted from this
|
|
24
|
+
* file byte-for-byte, and `pnpm lint:plugin --check` fails if the two diverge.
|
|
25
|
+
*
|
|
26
|
+
* The plugin copy is what reaches a project scaffolded before this hook existed.
|
|
27
|
+
* When a project owns its own copy, the plugin's stays silent — see
|
|
28
|
+
* `isPluginCopy()` below — so a scaffolded project announces itself once.
|
|
24
29
|
*
|
|
25
30
|
* ## Deliberately silent, and deliberately offline
|
|
26
31
|
*
|
|
@@ -33,8 +38,9 @@
|
|
|
33
38
|
*
|
|
34
39
|
* Opt out by creating `.substrat/no-session-context`.
|
|
35
40
|
*/
|
|
36
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
37
|
-
import { dirname, join } from 'node:path';
|
|
41
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
42
|
+
import { dirname, join, relative, isAbsolute } from 'node:path';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
38
44
|
|
|
39
45
|
const DOCS = 'https://substrat.net';
|
|
40
46
|
const KERNEL = '@substrat-run/kernel';
|
|
@@ -42,6 +48,33 @@ const KERNEL = '@substrat-run/kernel';
|
|
|
42
48
|
/** Where the project is. Claude Code sets this; fall back to the cwd it ran us in. */
|
|
43
49
|
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
44
50
|
|
|
51
|
+
/** Where this script is. The two copies differ only in where they sit on disk. */
|
|
52
|
+
const self = fileURLToPath(import.meta.url);
|
|
53
|
+
|
|
54
|
+
/** Symlinks resolved, so a project reached through one is not mistaken for elsewhere. */
|
|
55
|
+
function real(path) {
|
|
56
|
+
try {
|
|
57
|
+
return realpathSync(path);
|
|
58
|
+
} catch {
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when we are the plugin's copy rather than the project's own.
|
|
65
|
+
*
|
|
66
|
+
* Asked positionally instead of by a flag, because the two copies must stay
|
|
67
|
+
* byte-identical for `lint:plugin` to have anything to check: a copy that is
|
|
68
|
+
* invoked differently is a copy that can be edited differently. Both sides are
|
|
69
|
+
* resolved first — on macOS a project under `/tmp` is handed to us as
|
|
70
|
+
* `/private/tmp`, and an unresolved compare would read the project's own copy as
|
|
71
|
+
* foreign and silence the very hook that should speak.
|
|
72
|
+
*/
|
|
73
|
+
function isPluginCopy() {
|
|
74
|
+
const inside = relative(real(root), real(self));
|
|
75
|
+
return inside.startsWith('..') || isAbsolute(inside);
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
/** The marker: which kernel version this project last announced. */
|
|
46
79
|
const MARKER = join(root, '.substrat', '.docs-pin');
|
|
47
80
|
const OPT_OUT = join(root, '.substrat', 'no-session-context');
|
|
@@ -67,6 +100,11 @@ function main() {
|
|
|
67
100
|
// file to invent, and nothing to keep in sync.
|
|
68
101
|
if (!pkg?.substrat) silent();
|
|
69
102
|
|
|
103
|
+
// The project's own copy wins. It ships beside the playbook it points at, so it
|
|
104
|
+
// is the one that matches what is actually checked out here; the plugin's exists
|
|
105
|
+
// for projects scaffolded before this hook did, and must not double-announce.
|
|
106
|
+
if (isPluginCopy() && existsSync(join(root, '.substrat', 'hooks', 'session-start.mjs'))) silent();
|
|
107
|
+
|
|
70
108
|
if (existsSync(OPT_OUT)) silent();
|
|
71
109
|
|
|
72
110
|
// What is actually installed beats what package.json asked for: a caret range on
|
|
@@ -190,10 +190,21 @@ plain language, so nothing there is a surprise:
|
|
|
190
190
|
cross-tenant attacker gets nothing).
|
|
191
191
|
9. **Open decisions** — each with a **recommended default**, so the user chooses rather
|
|
192
192
|
than specifies:
|
|
193
|
-
- **Auth.** Local dev uses an `x-principal` header — a dev seam, not a login.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
- **Auth.** Local dev uses an `x-principal` header — a dev seam, not a login. Real auth
|
|
194
|
+
gates *exposing* the app, not *building* it — but if the app will be deployed for real
|
|
195
|
+
users, wire the OIDC seam from the start: the standard is a **separate OIDC issuer**
|
|
196
|
+
(an Auth Server app in the same team, or an external issuer — Supabase/Auth0/AuthHero/
|
|
197
|
+
Keycloak), never per-app credential storage. The vertical is a pure OIDC **relying
|
|
198
|
+
party**: depend on `@substrat-run/vertical-auth`, bind its `IdentityDO` (the
|
|
199
|
+
`sub → principal` directory, TOFU owner claim, invites) as a third DO store, read the
|
|
200
|
+
platform-delivered `substrat:auth` config per scope (`authWiring`), and build
|
|
201
|
+
`oidcRpAuthProvider` per request — see https://substrat.net/concepts/identity. The
|
|
202
|
+
dashboard's New-app **Identity** section does the automatic half (dynamic client
|
|
203
|
+
registration at the issuer + `/internal/configure` delivery) — but ONLY at app
|
|
204
|
+
creation, and only for verticals that implement this seam. An install created without
|
|
205
|
+
the identity choice stays `builtin`/unwired forever (switching issuers is deliberately
|
|
206
|
+
create-time only); a starter worker whose `authenticatedPrincipal` returns null answers
|
|
207
|
+
401 to everything deployed, however many auth servers exist in the team.
|
|
197
208
|
- **Deploy or stay local.** Local-first is a legitimate endpoint; default to it.
|
|
198
209
|
10. **Out of scope / deferred** — what you are deliberately not building, so the review is
|
|
199
210
|
about a bounded thing.
|
|
@@ -428,7 +439,9 @@ only in seed.ts would run locally and silently not deploy. The deploy path is th
|
|
|
428
439
|
authenticated CLI, and the author never holds a Cloudflare token:
|
|
429
440
|
|
|
430
441
|
- `substrat login` / `substrat whoami` — authenticate against the control plane.
|
|
431
|
-
- `substrat push` — push the vertical
|
|
442
|
+
- `substrat push` — push the vertical. By default the version is the registry's highest
|
|
443
|
+
semver, patch-bumped; `package.json`'s version is only a **seed for the first push of a
|
|
444
|
+
new slug**, and an explicit `--version` always wins. A **private** (tenant-owned)
|
|
432
445
|
vertical is admitted automatically; a **listed/shared** one waits for staff admission.
|
|
433
446
|
- `substrat promote <slug> --channel dev|staging|prod --version … [--ack-permissions]
|
|
434
447
|
[--ack-migrations]` — the owner promotes every channel, prod included, for their own
|
|
@@ -436,6 +449,50 @@ authenticated CLI, and the author never holds a Cloudflare token:
|
|
|
436
449
|
- `substrat hostnames bind <slug> --surface <s> [--domain <d>]` — mint a live hostname, or
|
|
437
450
|
record a custom domain pending DNS validation (`substrat hostnames verify`).
|
|
438
451
|
|
|
452
|
+
**A SPA ships as NATIVE assets — never inline it into the worker.** Declare it in
|
|
453
|
+
`runtimeNeeds` and `substrat push` builds, hashes, and uploads the directory to the
|
|
454
|
+
runtime's own asset store, served from the edge without invoking the worker:
|
|
455
|
+
|
|
456
|
+
```jsonc
|
|
457
|
+
"substrat": {
|
|
458
|
+
"runtimeNeeds": {
|
|
459
|
+
"entry": "src/worker.ts",
|
|
460
|
+
"build": "npm --prefix app install && npm --prefix app run build",
|
|
461
|
+
"assets": {
|
|
462
|
+
"directory": "app/dist",
|
|
463
|
+
"notFoundHandling": "single-page-application", // deep client routes → index.html
|
|
464
|
+
"runWorkerFirst": ["/api/*", "/internal/*"] // only these reach the worker
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`build` runs before assets are collected, so the directory may be pure build output. Never
|
|
471
|
+
base64-inline a built `app/dist` into a generated worker module: it costs ~+33 % script
|
|
472
|
+
size and a worker invocation per image.
|
|
473
|
+
|
|
474
|
+
**Let changesets own the version, and pass it to push explicitly** — the default bump walks
|
|
475
|
+
the registry forward on its own, so `package.json` and the registry drift apart within a
|
|
476
|
+
few deploys. Set this up when the vertical first deploys:
|
|
477
|
+
|
|
478
|
+
```sh
|
|
479
|
+
pnpm add -D @changesets/cli && npx changeset init
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Then in `.changeset/config.json` add `"privatePackages": { "version": true, "tag": false }`
|
|
483
|
+
(the vertical is a private package, never npm-published), make sure `pnpm-workspace.yaml`
|
|
484
|
+
lists `packages: ["."]` so changesets can see the root package, and add the scripts:
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
"changeset": "changeset",
|
|
488
|
+
"release": "pnpm test && pnpm typecheck && pnpm lint:boundaries && changeset version && substrat push --version $(node -p \"require('./package.json').version\") --promote prod"
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Read the version with `node -p` at that point in the script, not `$npm_package_version` —
|
|
492
|
+
the latter is captured before `changeset version` rewrites `package.json`, so it would push
|
|
493
|
+
the version you just replaced. Changesets needs a git repo with a commit on the base branch
|
|
494
|
+
(`git init -b main`) — a scaffold that isn't a repo yet must init before the first release.
|
|
495
|
+
|
|
439
496
|
Updates deploy **in place** from one stable script — data carries forward, migrations run
|
|
440
497
|
against prod data, backout is a time-boxed PITR rewind.
|
|
441
498
|
|