create-objectstack 17.1.0 → 17.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,454 @@
1
1
  # create-objectstack
2
2
 
3
+ ## 17.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 366f895: feat(auth): migrate `@better-auth/scim` from `1.7.0-rc.1` to stable `1.7.1` — the whole-model SCIM migration (#3653, epic #11632)
8
+
9
+ The stable line is the rc.2-lineage rewrite: the rc.1 `scimProvider` model,
10
+ `/scim/generate-token` endpoint and `storeSCIMToken` option no longer exist,
11
+ replaced by seven new models and a three-way connection contract. This lands
12
+ the migration atomically:
13
+
14
+ - **Seven new platform objects** back the stable models —
15
+ `sys_scim_connection_binding`, `sys_scim_group`, `sys_scim_group_member`,
16
+ `sys_scim_identity_tombstone`, `sys_scim_projection_grant`,
17
+ `sys_scim_subject`, `sys_scim_user` — bridged via `AUTH_MODEL_TO_PROTOCOL`,
18
+ registered in the platform-object-names registry, listed in
19
+ `BETTER_AUTH_MANAGED_OBJECTS`, and column-pinned by the parity gate (whose
20
+ `KNOWN_UNMAPPED_MODELS` shrinks to the empty set: the rc.1-era group
21
+ provisioning gap — IdP `/Groups` pushes hitting tables that did not exist —
22
+ is closed).
23
+ - **SCIM connections stay runtime data.** The stable constructor is satisfied
24
+ with an application-owned `authentication.verifyBearerToken` that resolves
25
+ the connection from a row at request time — not static boot config, and not
26
+ the upstream `managedConnections` catalog (deliberately not adopted).
27
+ - **ObjectStack owns SCIM credentials outright** (stable upstream stores no
28
+ credential at all): `sys_scim_connection_credential` plus
29
+ `scim-connection-service.ts` mint/digest/verify. At rest only an
30
+ HMAC-SHA-256 keyed by the deployment auth secret (base64url,
31
+ domain-separated) is stored — at parity or better than the rc.1 unsalted
32
+ SHA-256 — pinned by `credential-at-rest-posture.test.ts` including live
33
+ 401 paths for forged, revoked and expired bearers.
34
+ - **The ObjectQL better-auth adapter gains native transactions**
35
+ (`engine.transaction`, fail-closed on drivers without `beginTransaction`),
36
+ which stable scim requires by assertion for atomic provisioning writes.
37
+ - **Scaffold suppression retired**: the `@better-auth/scim>better-call`
38
+ `allowedVersions` entry (CLI renderer + blank template) is gone — stable
39
+ 1.7.1 peers `better-call@1.4.0` exactly — and its presence ratchets flipped
40
+ to absence pins. The `better-auth>better-sqlite3` and four
41
+ `@better-auth/utils` entries stay; their retirement conditions are separate
42
+ and unmet.
43
+ - The pin resolves **1.7.1 exactly** (not `^1.7.1`): 1.7.2 peers
44
+ `better-auth`/`@better-auth/core` at `^1.7.2`, which only the workspace
45
+ overrides' silencing would "satisfy" while the family is 1.7.1. Floating is
46
+ its own follow-up.
47
+
48
+ **Semver: minor, argued.** The rc.1 SCIM surface this replaces (generate-token
49
+ endpoint, rc.1 bearer tokens, `sys_scim_provider` rows) changes incompatibly —
50
+ but that surface is default-off (`OS_SCIM_ENABLED`), was shipped with a
51
+ documented "do not let the IdP push groups" boundary, and the maintainer ruled
52
+ (2026-08-25) that SCIM has no real customers and old data need not carry: the
53
+ one binding constraint is that an existing system upgrades smoothly, which it
54
+ does — every table the installed library can write exists at this version, and
55
+ SCIM-disabled deployments see no behavior change. A major would move the whole
56
+ fixed version group for a feature surface with zero consumers. Deployments
57
+ that had SCIM enabled must mint new connection credentials (digests are not
58
+ portable from rc.1 on any path — IdP token reissue is a migration-day
59
+ operator action regardless of semver level). `sys_scim_provider` itself is
60
+ NOT removed here; its retirement is tracked separately (#11757).
61
+
62
+ ### Patch Changes
63
+
64
+ - c6c7fec: fix(create-objectstack): stop reporting a failed pnpm probe as a deliberate npm choice (#11616)
65
+
66
+ `detectPackageManager()` was `try { execSync('pnpm --version') } catch { return
67
+ 'npm' }`, so every failure mode collapsed into one answer. `npm install` in the
68
+ scaffolder's output meant either *this machine has no pnpm* or *the probe
69
+ threw*, and nothing — no log line, no message — could tell the two apart.
70
+
71
+ That second case is reachable on an ordinary developer machine, not just in
72
+ theory: `pnpm --version` resolves through Corepack and therefore depends on the
73
+ directory it runs in. Measured on one machine, one binary, two directories —
74
+ `10.31.0` inside a repo that pins `packageManager`, `10.33.0` outside it, where
75
+ Corepack has to resolve, and may have to fetch, a version nothing pinned. A
76
+ user who has pnpm installed but is on a slow or offline network was silently
77
+ told to run npm.
78
+
79
+ The probe now reports why as well as what:
80
+
81
+ - `probe: 'ok'` — pnpm answered, so pnpm is used (unchanged, silent).
82
+ - `probe: 'absent'` — no pnpm on PATH at all, so npm is a real choice
83
+ (unchanged, silent).
84
+ - `probe: 'failed'` — pnpm **is** on PATH and the probe still threw. npm is
85
+ used exactly as before, and the run now says so, naming the underlying
86
+ failure: `pnpm is installed but \`pnpm --version\` failed (<reason>); using
87
+ npm as a fallback.`
88
+
89
+ **Which package manager a run uses is unchanged in all three cases** — it is
90
+ still pnpm if and only if the probe succeeded. The PATH lookup that separates
91
+ `absent` from `failed` runs only after the decision is already made and feeds
92
+ the message alone, so a miss there can change a warning's wording and never the
93
+ tool's behaviour. The only output that moves is one warning in a case that was
94
+ previously silent and wrong.
95
+
96
+ ## 17.2.0
97
+
98
+ ### Minor Changes
99
+
100
+ - 5a616d5: `create-objectstack` now closes with a "Created files" summary derived from a
101
+ walk of the finished project directory, so it names everything the run wrote —
102
+ including the files written after the template copy (#10323).
103
+
104
+ The old summary was the template copy's own list, printed before
105
+ `<pm> install` and before `npx skills add`. Measured against published
106
+ `create-objectstack@17.1.0` (`create-objectstack demo-app`, then a full walk of
107
+ the result): 12 entries printed, 18,045 paths on disk, **18,033 of them
108
+ unreachable from the summary** — `AGENTS.md`, `.github/copilot-instructions.md`,
109
+ `pnpm-lock.yaml`, `skills-lock.json`, `node_modules/`, and two ~968 KB trees of
110
+ agent instructions at `.agents/skills/` and `agent/skills/`.
111
+
112
+ That mattered because the same run ends with the `skills` CLI printing *"Review
113
+ skills before use; they run with full agent permissions."* Advice to review
114
+ files the run never named, at paths it never showed, is advice a newcomer
115
+ cannot act on — the wrong failure direction for a security-flavoured warning.
116
+
117
+ The list could not have been correct where it stood: two of the three write
118
+ phases belong to other processes, and the `skills` installer's destination set
119
+ moves with **its** releases, not ours. Reading the directory afterwards makes
120
+ the summary self-correcting instead. Large directories collapse to one line
121
+ carrying their path, entry count and size, so the bulk stays reviewable without
122
+ 18,000 lines of output, and the paths the skills installer created are marked
123
+ `⚠ skills` with the permissions warning tied to them.
124
+
125
+ Same run, after the change: 20 entries printed, **0 written paths unreachable**.
126
+
127
+ ### Patch Changes
128
+
129
+ - cec9d23: Fix `create-objectstack`'s startup banner hardcoding `◆ Create ObjectStack v6.x`
130
+ regardless of the package's real, released version — eleven majors stale, on
131
+ the first line of output a newcomer ever sees (#10325). The banner now calls
132
+ `readCliVersion()`, the same reader `.version()` already used, instead of a
133
+ literal string.
134
+
135
+ Dropping the real version in without recomputing the box's padding would have
136
+ reintroduced the same defect one line later — the border is a fixed run of
137
+ `═` computed for the 4-character `v6.x`, and a longer real version (`v17.1.0`
138
+ is 7 characters) would push the right border out of alignment (the sibling
139
+ bug fixed in #10322, one function away in the same file). The box now derives
140
+ its width from the version string's plain length and widens the frame — never
141
+ truncates — for a version long enough to need more room; ordinary versions
142
+ still render at the historical box size.
143
+
144
+ No behaviour change beyond the printed banner.
145
+ - 3a3f209: Tell a newcomer that the `blank` starter ships no app, so an empty Console
146
+ reads as the intended starting point rather than a broken install (#10317).
147
+
148
+ Measured on a real scaffold-and-boot (`create-objectstack my-app -t blank`,
149
+ published 17.1.0 packages, `objectstack dev --ui`): `GET /api/v1/meta/app`
150
+ returns the two platform apps (Setup, Account) and nothing of the project's
151
+ own, while `GET /api/v1/data/my_app_note` serves the scaffolded object the
152
+ whole time. The template ships `src/objects/` only — deliberately, as every
153
+ scaffolder template in this repo does — but nothing the newcomer could reach
154
+ said so, and `pnpm dev` advertises the Console URL on every boot.
155
+
156
+ Documentation only: a new "The Console" section in the generated `README.md`
157
+ naming the Console path, the consequence, and `src/apps/*.app.ts` as the
158
+ remedy. No change to what the scaffolder writes into `src/`.
159
+ - 7bf3fb7: Point every documentation link in these packages' published READMEs — and in
160
+ the project `create-objectstack` scaffolds — at the canonical docs origin
161
+ `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling.
162
+
163
+ Both spellings reach the same pages (the alias redirects to the apex,
164
+ path-preserving), so no link was broken. The reason it needs a release rather
165
+ than an in-repo fix alone: a README ships inside the npm tarball, so the
166
+ version already on npm keeps showing the old host to every reader of the
167
+ package page until a new one is published.
168
+ - 675ab57: **First-run polish:** a brand-new scaffold's very first `pnpm install` no longer reports two unmet peer dependencies (#10326).
169
+
170
+ Reproduced on a clean scaffold from published `create-objectstack@17.1.0` — no lockfile, `node_modules` removed, nothing configured by the user — and again on the second scaffold path, `objectstack init`. Both printed the same two:
171
+
172
+ ```
173
+ ✕ unmet peer better-call
174
+ Installed: 1.4.0
175
+ Wanted:
176
+ 1.3.7:
177
+ @better-auth/scim@1.7.0-rc.1
178
+
179
+ ✕ unmet peer better-sqlite3
180
+ Installed: 13.0.3
181
+ Wanted:
182
+ ^12.0.0:
183
+ better-auth@1.7.1
184
+ ```
185
+
186
+ Nothing was broken — but it is the first screen a newcomer sees, and there is nothing they did to cause it or can do about it.
187
+
188
+ **`better-sqlite3`: the pin is right and the upstream range is stale — so it is widened, not corrected.** better-auth 1.7.1 declares `better-sqlite3` as an **optional** peer at `^12.0.0`, and it governs exactly one configuration: a raw better-sqlite3 `Database` handed to better-auth's `database` option, which its Kysely dialect then drives. ObjectStack never takes that path — `AuthManager.createDatabaseConfig()` returns `createObjectQLAdapterFactory(dataEngine)`, and every `better-sqlite3` use under `plugin-auth` is knex's `client: 'better-sqlite3'` beneath ObjectQL. Measured anyway on the configuration the range *does* govern: better-auth 1.7.1 with `database: new Database(':memory:')`, running `getMigrations().runMigrations()`, `signUpEmail`, `signInEmail` and adapter `findOne`/`update`/`delete`, is green on **better-sqlite3 13.0.3** and byte-for-byte equivalent on **12.11.1**. The same probe with `Database.prototype.prepare` neutered fails, so that green is the driver's and not an unexercised path. Pinning our own `^13.0.3` declarations back to `^12` would downgrade a native module across the platform to satisfy a range measurement shows is simply behind.
189
+
190
+ **`@better-auth/scim`: the rc pin stays, and one `better-call` copy is the correct tree.** `npm view @better-auth/scim dist-tags` reads `latest: '1.7.1'`, but stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is a separate migration rather than a version bump; the exact `1.7.0-rc.1` pin is deliberate. The rc peers an exact `better-call@1.3.7` while better-auth 1.7.1 depends on `1.4.0` — and a better-auth plugin has to share the **host's** better-call instance, so the single 1.4.0 copy every install already resolves is right, not a skew to repair. This declaration retires together with the rc pin.
191
+
192
+ **What changed, and what deliberately did not.** Both remedies are pnpm `peerDependencyRules.allowedVersions` entries, scoped `<declaring package>><peer>` so each widens exactly one declaration. They ship *inside* the scaffold — the bundled `pnpm-workspace.yaml` template and the one `objectstack init` renders — because a block in this repo's own workspace file does not travel with published packages. `allowedVersions` changes what pnpm **reports**, never what it resolves: measured on both scaffold paths, the lockfile is byte-identical with and without it (0 lines of diff), and no dependency version, range or resolution moved anywhere. This repo's own resolutions are untouched.
193
+ - e85182d: Converge the blank scaffold template's `README.md` docs links on the ruled
194
+ canonical origin, `https://objectstack.ai` (maintainer ruling, 2026-08-21:
195
+ 「这个仓的文档站规范 URL 是 https://objectstack.ai」; enforced by
196
+ `CANONICAL_DOCS_ORIGIN` in `scripts/check-published-readme-links.mjs`). The
197
+ template previously linked the accepted-but-unratified `docs.objectstack.ai`
198
+ alias in three places, which disagreed with the root `README.md`'s already-
199
+ canonical spelling — so a single `npm create objectstack@latest` run handed
200
+ the user two different hostnames for the same docs site.
201
+ - aea1e64: Fix the declared bin (`bin/create-objectstack.js`) being tracked non-executable
202
+ in git. It carries a `#!/usr/bin/env node` shebang and is pnpm's link target
203
+ for the `create-objectstack` command, but was committed `100644` instead of
204
+ `100755` — matching the sibling declared bin `packages/cli/bin/run.js`, which
205
+ was already tracked executable.
206
+
207
+ Patch bump: this is a packaging-mode correction with no content, API or
208
+ behavior change (the blob hash is identical) — it only fixes how the file is
209
+ tracked in git and therefore how it is packed for npm.
210
+ - 818e027: Fix `objectstack init`'s closing "Created files" summary omitting `pnpm-lock.yaml` / `package-lock.json` and `node_modules/` (#10557).
211
+
212
+ The summary used to be printed from a list accumulated while the template
213
+ files were written — before `<pm> install` ran — so it could never name what
214
+ the package manager wrote. `init` now prints it after the install attempt
215
+ (succeeded or failed) from a walk of the finished project directory, reusing
216
+ `create-objectstack`'s `created-summary.ts` (now published as the
217
+ `create-objectstack/created-summary` subpath) instead of a second copy of the
218
+ same renderer.
219
+ - afe1c4e: fix(cli): declare the four `@better-auth/utils` peer skews a freshly scaffolded project reports (#10931)
220
+
221
+ Both scaffold paths emit a `peerDependencyRules.allowedVersions` block whose
222
+ stated purpose is that a brand-new project's first `pnpm install` does not open
223
+ with a peer-skew report. It declared two skews and left four showing:
224
+
225
+ ```
226
+ ├─┬ @better-auth/core 1.7.1
227
+ │ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
228
+ ├─┬ @better-auth/scim 1.7.0-rc.1
229
+ │ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
230
+ ├─┬ @better-auth/oauth-provider 1.7.1
231
+ │ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
232
+ └─┬ @better-auth/sso 1.7.1
233
+ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
234
+ ```
235
+
236
+ `@better-auth/core`, `/oauth-provider`, `/scim` and `/sso` each peer an **exact**
237
+ `@better-auth/utils@0.4.2`. The 0.5.0 they are handed comes from
238
+ `better-call@1.4.0` — better-auth's own HTTP layer — which *depends* on
239
+ `^0.5.0`; `@objectstack/plugin-auth` names the four as direct dependencies
240
+ without naming utils, so pnpm satisfies their peer from better-call's copy
241
+ instead of better-auth's own exact 0.4.2 dependency.
242
+
243
+ **Measured compatible before widening, not assumed.** Those four import three
244
+ symbols in total: `base64`/`base64Url` (`@better-auth/utils/base64`),
245
+ `createHash` (`/hash`) and, in core only, `createRandomStringGenerator`
246
+ (`/random`). 0.5.0 declares all three with identical signatures; `/random` is
247
+ unchanged apart from formatting, `/base64` swaps `new Uint8Array(data)` for a
248
+ helper that *is* `new Uint8Array(data)` on non-strings, and `/hash` only widens
249
+ its input coercion for views not backed by a plain `ArrayBuffer`. Run against
250
+ the input shapes those call sites actually pass, the two versions agree on every
251
+ value; run end to end — better-auth with the `sso`, `oauth-provider` and `scim`
252
+ plugins — a tree where the four resolve 0.5.0 and one where they resolve 0.4.2
253
+ produce the same transcript: sign-up, sign-in, session, both OAuth metadata
254
+ documents, the RFC 7636 PKCE challenge, and the SCIM and SSO endpoint outcomes.
255
+
256
+ A resolution change was measured too, and rejected: pinning utils back to 0.4.2
257
+ clears the four lines only by dragging `better-call@1.4.0` off its own declared
258
+ `^0.5.0` — manufacturing one real range violation to silence four benign ones.
259
+
260
+ Four scoped entries, one per declaring package, matching the block's convention
261
+ that each rule widens exactly one declaration. `allowedVersions` suppresses the
262
+ report only: the lockfile a scaffold resolves is byte-identical with and without
263
+ the block. The version is spelled `0.5.0` exactly rather than `0.5`, so a future
264
+ `0.6.0` reports again instead of inheriting this finding.
265
+
266
+ Both scaffold paths — `objectstack init` (rendered by the CLI) and
267
+ `npx create-objectstack` (a copied template file) — are changed together, and
268
+ `packages/cli/test/scaffold-workspace-consistency.test.ts` gains a limb that
269
+ compares the peer maps the two produce, so they cannot drift apart again.
270
+ - 21756b3: fix(create-objectstack): converge scaffold docs on the canonical host and drop the last two dead monorepo references (#10990, #11022)
271
+
272
+ A freshly scaffolded project shipped a handful of text lines a reader with
273
+ only their own project — no monorepo, no `docs/adr/`, no issue tracker —
274
+ could not follow:
275
+
276
+ - `templates/AGENTS.md` linked `https://objectstack.com/docs`, a domain that
277
+ is not this project's docs site at all (not even a redirecting alias).
278
+ - `templates/blank/Dockerfile` and `templates/blank/docker-compose.yml` both
279
+ linked `https://docs.objectstack.ai/...`, an accepted-but-unratified alias.
280
+ All three now point at the ruled canonical origin, `https://objectstack.ai`
281
+ (maintainer ruling, 2026-08-21).
282
+ - `templates/blank/README.md` cited `ADR-0097` and named "the ObjectStack
283
+ framework repo" as the home of `skills/` — both rewritten self-contained,
284
+ keeping the fact each was carrying: the connector-materialization line now
285
+ links the public [Automation → Connectors](https://objectstack.ai/docs/automation/connectors)
286
+ page, and the skills line now names the followable
287
+ `npx skills add objectstack-ai/objectstack/skills` install the scaffolder's
288
+ own closing output already uses.
289
+
290
+ `packages/create-objectstack/src/starter-comments-self-contained.test.ts`
291
+ (#10324) gains two pin obligations these two fixes call for: a host-convergence
292
+ assertion driven by the same `shippedFiles()` walker that already enumerates
293
+ everything a scaffold ships (no other repo gate's population reaches these
294
+ template files — `check:published-readme-links` reads publishable packages'
295
+ published markdown only), and a fifth `MONOREPO_ONLY` pattern that catches a
296
+ prose-shaped reference to this repo ("the ObjectStack framework repo") the
297
+ first four, syntax-shaped patterns could not. The self-retiring `EXCLUDED`
298
+ entry for `blank/README.md` is removed now that the file cites nothing
299
+ monorepo-only.
300
+ - 568de19: Scaffolded projects declare an explicit empty `packages: []` in their
301
+ `pnpm-workspace.yaml` (#10933). Both scaffold paths render it —
302
+ `renderPnpmWorkspaceYaml` in `objectstack init`, and the bundled `blank`
303
+ template `npx create-objectstack` copies.
304
+
305
+ The file was deliberately keyless so it would act purely as a settings file.
306
+ That intent is now written down rather than inferred from a missing key, and
307
+ writing it down is what fixes a first-command failure: pnpm 9.x and 10.0–10.4
308
+ parse `pnpm-workspace.yaml` **before** they read `engines`, so they refused a
309
+ brand-new project outright with
310
+
311
+ ```
312
+ ERROR packages field missing or empty
313
+ ```
314
+
315
+ naming a file the user never wrote and giving no hint that the cause is their
316
+ pnpm version — and no `engines.pnpm` floor could reach them, because they never
317
+ got as far as the engines check. Measured, one clean install per pnpm version,
318
+ each with its own store:
319
+
320
+ | pnpm | before | after |
321
+ |---|---|---|
322
+ | 9.15.9, 10.0.0, 10.4.0 | `ERROR packages field missing or empty` | `ERR_PNPM_UNSUPPORTED_ENGINE`, naming `>=10.15` |
323
+ | 10.5.0–10.14.0 | `ERR_PNPM_UNSUPPORTED_ENGINE` | unchanged |
324
+ | 10.15.0, 10.34.5, 11.22.0 | installs | installs, byte-identical `pnpm-lock.yaml` |
325
+
326
+ So every unsupported pnpm now reports the same actionable cause, and supported
327
+ pnpm is unaffected: the empty key was measured equivalent to omission on
328
+ 10.15.0, 10.34.5 and 11.22.0 — identical lockfile bytes, identical
329
+ `node_modules/.modules.yaml` once the run-local `prunedAt`/`storeDir` fields are
330
+ dropped, identical `pnpm ls -r --depth -1`, and an identical second-install
331
+ "Already up to date".
332
+
333
+ The declaration is an **empty** list on purpose. `packages: ['.']` satisfies the
334
+ same parsers but declares the project root a workspace *member* — a monorepo
335
+ root — which a single-package scaffold is not, and which reads to the next
336
+ author (human or AI) as an invitation to add member packages to an app.
337
+
338
+ `engines.pnpm` is unchanged at `>=10.15`.
339
+ - 8d21f7a: Fix `create-objectstack`'s closing "Next steps" and install-failure remedy
340
+ hardcoding `npm` regardless of which package manager the run actually used
341
+ (#10322). `detectPackageManager()` already prefers `pnpm` and falls back to
342
+ `npm` only when `pnpm` is unreachable — confirmed still true at HEAD, and
343
+ confirmed empirically: a real run with `pnpm` on `PATH` installs with `pnpm`
344
+ (`pnpm-lock.yaml`, "Done in … using pnpm vX") and then told the newcomer to
345
+ run `npm run dev` / `npm run validate` next, a package manager the run never
346
+ touched. The detected package manager is now read once, up front, and reused
347
+ consistently for the install command, the install-failure remedy, and every
348
+ line of "Next steps" — so the printed guidance always names the tool the run
349
+ actually used, in both the `pnpm` and the `npm`-fallback case.
350
+
351
+ Also names `validate` — the step the generated `AGENTS.md` calls
352
+ unskippable — in the "Getting started" section of the generated `blank`
353
+ template's README, not only in its later "Verify your changes" section, so a
354
+ newcomer reading top-to-bottom sees it at first touch.
355
+
356
+ No install behaviour changes: the scaffolder still installs by default and
357
+ still supports `--skip-install`; this is a messaging-only fix.
358
+ - 9d101d2: Declare a pnpm floor (`engines.pnpm: ">=10.15"`) in the `package.json` both
359
+ scaffolders write, so an unsupported pnpm reports its own version instead of an
360
+ error about a file the user never wrote.
361
+
362
+ Both scaffold paths emit a settings-only `pnpm-workspace.yaml` with no
363
+ `packages:` key. Early pnpm 10 refuses that file outright — `pnpm install` exits
364
+ 1 with `ERROR packages field missing or empty` before resolving a single
365
+ dependency, so a brand-new project could not be installed at all. Measured on
366
+ the rendered shape, one clean install per pnpm version, each with its own store:
367
+
368
+ | pnpm | before | after |
369
+ | --- | --- | --- |
370
+ | 10.0.0 – 10.4.0 | `packages field missing or empty` | unchanged — see below |
371
+ | 10.5.0 – 10.14.0 | `packages field missing or empty` | `ERR_PNPM_UNSUPPORTED_ENGINE`, naming the expected range |
372
+ | >= 10.15.0 | installs | installs |
373
+
374
+ The floor is a diagnosis, not a repair: pnpm 10.0.0–10.4.0 parse
375
+ `pnpm-workspace.yaml` *before* they read `engines`, so they still print the raw
376
+ workspace error. Closing that remaining sliver requires deciding what a
377
+ single-package scaffold should declare under `packages:`, which is tracked
378
+ separately and deliberately not decided here.
379
+
380
+ `engines.pnpm` rather than a `packageManager` stamp: npm, yarn and bun ignore
381
+ `engines.pnpm` entirely, so the scaffold keeps working for all four package
382
+ managers `objectstack init` hands off to. A `packageManager: "pnpm@x.y.z"` stamp
383
+ would declare the project pnpm-only (corepack-driven yarn refuses to run in such
384
+ a project) and pin one exact version that goes stale on every pnpm release — and
385
+ it buys nothing on 10.0–10.4, which reach the workspace error before reading
386
+ that field either.
387
+
388
+ No existing project is affected; this only changes what a newly scaffolded
389
+ `package.json` contains.
390
+ - 6d441e4: Correct the pnpm boundary the blank template states for `allowBuilds`, and gate
391
+ the two scaffold paths against each other (#10498, #10499).
392
+
393
+ `packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml` is copied
394
+ verbatim into every scaffolded project, so its header comment is prose that
395
+ ships **inside the user's own repository**. It said `allowBuilds` needs
396
+ pnpm >= 10.31 and that `onlyBuiltDependencies` covers pnpm 10.0–10.30. Measured
397
+ on a probe depending on `esbuild@0.28.2`, with a workspace file carrying only
398
+ `allowBuilds`, one clean install per pnpm version and each with its own
399
+ `--store-dir` (isolation matters — pnpm's side-effects cache will otherwise hand
400
+ a later run a build an earlier run performed, and it reads as "the key worked"):
401
+
402
+ | pnpm | `allowBuilds` alone |
403
+ |:--|:--|
404
+ | 10.15.0 – 10.25.0 | ignored — build not run |
405
+ | **10.26.0** | **honoured — build ran** |
406
+ | 10.28.0 – 10.33.0 | honoured — build ran |
407
+
408
+ So the floor is 10.26.0 and the older-key band is 10.0–10.25. A user on pnpm
409
+ 10.28 was being told by the file in front of them that their pnpm cannot read
410
+ the key it is in fact reading. Both load-bearing claims in that comment were
411
+ correct and are unchanged: both keys are needed, and pnpm 11 reads only
412
+ `allowBuilds`. No setting, no assertion and no install behaviour changes — the
413
+ rendered `onlyBuiltDependencies` / `allowBuilds` values are byte-identical.
414
+
415
+ The reason it was wrong for so long is the second half of this change.
416
+ `objectstack init` renders the same file from `renderPnpmWorkspaceYaml()` in
417
+ `packages/cli`, it was corrected to the measured numbers separately, and each
418
+ package's ratchets are package-local — so neither could ever fail for the other
419
+ file's regression, and the two scaffold paths shipped contradictory prose about
420
+ the same rule with every gate green. `packages/cli/test/scaffold-workspace-consistency.test.ts`
421
+ now compares the two **rendered outputs**: the packages each key actually grants
422
+ a build to, and the pnpm versions each file actually names for each key. It was
423
+ confirmed failing against the live divergence before this correction landed.
424
+
425
+ Bumped `patch` rather than left out: the corrected text is user-visible — it is
426
+ delivered into every new project — while nothing executable moves.
427
+ - ecd06f6: Rewrite the scaffolded project's starter comments so a newcomer can actually
428
+ follow them (#10324). `objectstack.config.ts` and `src/objects/note.object.ts`
429
+ are the first two files opened after scaffolding, and between them they cited
430
+ four ADR identifiers, one bare issue number and the path of a release-time
431
+ script in this monorepo — none of which ship in, or are linked from, a
432
+ scaffolded project. `// per ADR-0097` read as a reference the reader was
433
+ failing to follow rather than as the context it was meant to be.
434
+
435
+ The explanations are kept and made self-contained; only the dead ends are
436
+ gone. Each now states the fact the identifier stood for — the protocol range
437
+ is checked before anything loads and was stamped to match the installed
438
+ version rather than hand-tuned; `automation` must stay whenever `plugins:`
439
+ lists a connector or the executors have nowhere to register; a declarative
440
+ `mcp` stdio transport is denied by default; the org-wide default is required
441
+ so the baseline is an authored decision — and points at the public docs page
442
+ that covers it in full. The blank `Dockerfile` likewise stops pointing at a
443
+ file in this repo and points at the self-hosting guide it already links.
444
+
445
+ A pin (`starter-comments-self-contained.test.ts`) keeps it that way from both
446
+ sides: no shipped template file may cite an ADR identifier, a bare issue
447
+ number or a repo script path, and the facts those references carried must
448
+ still be stated — so the comments cannot be "fixed" by deleting them. It also
449
+ resolves every canonical-origin docs URL in the shipped tree against
450
+ `content/docs`, because a link that 404s is the same defect one level out.
451
+
3
452
  ## 17.1.0
4
453
 
5
454
  ### Minor Changes
package/README.md CHANGED
@@ -87,7 +87,7 @@ npm run validate # verify metadata: schema + predicates + bindings
87
87
  ```
88
88
 
89
89
  See the docs:
90
- [Your First Project](https://docs.objectstack.ai/docs/getting-started/your-first-project).
90
+ [Your First Project](https://objectstack.ai/docs/getting-started/your-first-project).
91
91
 
92
92
  ## License
93
93
 
@@ -0,0 +1,112 @@
1
+ // src/created-summary.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ var COLLAPSE_AT = 10;
5
+ var MEASURE_BUDGET = 2e3;
6
+ function scan(abs, name, budget) {
7
+ let st;
8
+ try {
9
+ st = fs.lstatSync(abs);
10
+ } catch {
11
+ return { name, dir: false, entries: 0, bytes: 0, truncated: false, children: [] };
12
+ }
13
+ if (!st.isDirectory()) {
14
+ budget.left -= 1;
15
+ return { name, dir: false, entries: 1, bytes: st.size, truncated: false, children: [] };
16
+ }
17
+ const node = { name, dir: true, entries: 0, bytes: 0, truncated: false, children: [] };
18
+ let dirents;
19
+ try {
20
+ dirents = fs.readdirSync(abs, { withFileTypes: true });
21
+ } catch {
22
+ return node;
23
+ }
24
+ for (const entry of dirents) {
25
+ if (budget.left <= 0) {
26
+ node.truncated = true;
27
+ break;
28
+ }
29
+ const child = scan(path.join(abs, entry.name), entry.name, budget);
30
+ node.entries += child.entries;
31
+ node.bytes += child.bytes;
32
+ if (child.truncated) node.truncated = true;
33
+ node.children.push(child);
34
+ }
35
+ return node;
36
+ }
37
+ function byName(a, b) {
38
+ return a.name.localeCompare(b.name, "en");
39
+ }
40
+ function flatten(node, prefix, out) {
41
+ for (const child of [...node.children].sort(byName)) {
42
+ const rel = prefix + child.name;
43
+ if (!child.dir) {
44
+ out.push({ path: rel, kind: "file", entries: 1, bytes: child.bytes, truncated: false });
45
+ continue;
46
+ }
47
+ if (!child.truncated && child.entries <= COLLAPSE_AT) {
48
+ flatten(child, `${rel}/`, out);
49
+ continue;
50
+ }
51
+ let deepest = child;
52
+ let shown = rel;
53
+ while (deepest.children.length === 1 && deepest.children[0].dir) {
54
+ deepest = deepest.children[0];
55
+ shown = `${shown}/${deepest.name}`;
56
+ }
57
+ out.push({
58
+ path: `${shown}/`,
59
+ kind: "dir",
60
+ entries: deepest.entries,
61
+ bytes: deepest.bytes,
62
+ truncated: deepest.truncated
63
+ });
64
+ }
65
+ }
66
+ function summarizeTree(root) {
67
+ let dirents;
68
+ try {
69
+ dirents = fs.readdirSync(root, { withFileTypes: true });
70
+ } catch {
71
+ return [];
72
+ }
73
+ const top = { name: "", dir: true, entries: 0, bytes: 0, truncated: false, children: [] };
74
+ for (const entry of dirents) {
75
+ top.children.push(scan(path.join(root, entry.name), entry.name, { left: MEASURE_BUDGET }));
76
+ }
77
+ const out = [];
78
+ flatten(top, "", out);
79
+ return [
80
+ ...out.filter((e) => e.kind === "file"),
81
+ ...out.filter((e) => e.kind === "dir")
82
+ ];
83
+ }
84
+ function formatBytes(bytes) {
85
+ if (bytes < 1024) return `${bytes} B`;
86
+ const kb = bytes / 1024;
87
+ if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
88
+ const mb = kb / 1024;
89
+ return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
90
+ }
91
+ function describeEntry(entry) {
92
+ if (entry.kind === "file") return "";
93
+ const noun = entry.entries === 1 ? "file" : "files";
94
+ if (entry.truncated) return `over ${entry.entries.toLocaleString("en-US")} ${noun}`;
95
+ return `${entry.entries.toLocaleString("en-US")} ${noun}, ${formatBytes(entry.bytes)}`;
96
+ }
97
+ function unreachablePaths(entries, written) {
98
+ const named = new Set(entries.filter((e) => e.kind === "file").map((e) => e.path));
99
+ const dirs = entries.filter((e) => e.kind === "dir").map((e) => e.path);
100
+ return written.filter(
101
+ (p) => !named.has(p) && !dirs.some((d) => `${p}/`.startsWith(d))
102
+ );
103
+ }
104
+
105
+ export {
106
+ COLLAPSE_AT,
107
+ MEASURE_BUDGET,
108
+ summarizeTree,
109
+ formatBytes,
110
+ describeEntry,
111
+ unreachablePaths
112
+ };
@@ -0,0 +1,48 @@
1
+ /** A directory holding more than this many entries is collapsed to one line. */
2
+ declare const COLLAPSE_AT = 10;
3
+ /**
4
+ * Entries this module is willing to `lstat` per top-level entry before it
5
+ * stops counting and reports a lower bound.
6
+ *
7
+ * The budget is PER TOP-LEVEL ENTRY, not global, and that is load-bearing:
8
+ * with one shared budget, `node_modules/` (17,920 paths in the measurement
9
+ * above) exhausts it before the walk reaches the project's own files, and the
10
+ * summary silently truncates the very content it exists to disclose. Whether
11
+ * that happened would depend on `readdir` order.
12
+ */
13
+ declare const MEASURE_BUDGET = 2000;
14
+ interface SummaryEntry {
15
+ /** Project-relative path. Directories carry a trailing `/`. */
16
+ path: string;
17
+ kind: 'file' | 'dir';
18
+ /** Files and symlinks in the subtree (always 1 for a file). */
19
+ entries: number;
20
+ /** Total size in bytes. Meaningless when `truncated`. */
21
+ bytes: number;
22
+ /** Measurement stopped at the budget — `entries` and `bytes` are lower bounds. */
23
+ truncated: boolean;
24
+ }
25
+ /**
26
+ * Summarize everything under `root`, collapsing large directories.
27
+ *
28
+ * Returns files first (alphabetical), then collapsed directories
29
+ * (alphabetical), so the enumerated content reads as a list and the bulk
30
+ * trees read as a block with their sizes.
31
+ */
32
+ declare function summarizeTree(root: string): SummaryEntry[];
33
+ /** Human-readable byte count. */
34
+ declare function formatBytes(bytes: number): string;
35
+ /** The measurement note that follows a collapsed directory's path. */
36
+ declare function describeEntry(entry: SummaryEntry): string;
37
+ /**
38
+ * The property this module exists to hold: every path in `written` is either
39
+ * named outright by a summary entry, or lies beneath a directory entry that
40
+ * is. Returns the paths that are NOT reachable — empty means the summary is
41
+ * complete.
42
+ *
43
+ * Exported because it is the assertion, and an assertion that lives only in a
44
+ * test file cannot be run against a real scaffold from anywhere else.
45
+ */
46
+ declare function unreachablePaths(entries: SummaryEntry[], written: string[]): string[];
47
+
48
+ export { COLLAPSE_AT, MEASURE_BUDGET, type SummaryEntry, describeEntry, formatBytes, summarizeTree, unreachablePaths };
@@ -0,0 +1,16 @@
1
+ import {
2
+ COLLAPSE_AT,
3
+ MEASURE_BUDGET,
4
+ describeEntry,
5
+ formatBytes,
6
+ summarizeTree,
7
+ unreachablePaths
8
+ } from "./chunk-ZIUW7UEA.js";
9
+ export {
10
+ COLLAPSE_AT,
11
+ MEASURE_BUDGET,
12
+ describeEntry,
13
+ formatBytes,
14
+ summarizeTree,
15
+ unreachablePaths
16
+ };