create-objectstack 17.0.0-rc.6 → 17.1.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 +350 -0
- package/README.md +8 -10
- package/dist/index.js +151 -129
- package/dist/templates/blank/Dockerfile +6 -3
- package/dist/templates/blank/objectstack.manifest.json +1 -1
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,355 @@
|
|
|
1
1
|
# create-objectstack
|
|
2
2
|
|
|
3
|
+
## 17.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1eb28a1: Retire the five remote content templates from the scaffolder's catalog.
|
|
8
|
+
|
|
9
|
+
`todo`, `compliance`, `content`, `contracts` and `procurement` were delisted
|
|
10
|
+
from the official ObjectStack template marketplace and are no longer
|
|
11
|
+
maintained, but the CLI carried its own hardcoded catalog and never learned
|
|
12
|
+
that: `--help` recommended all five by name with marketing descriptions, and
|
|
13
|
+
the `Available:` line on a bad `-t` offered them too.
|
|
14
|
+
|
|
15
|
+
- `blank` (bundled, offline) is now the whole catalog, so the help text
|
|
16
|
+
advertises only what is actually supported.
|
|
17
|
+
- Asking for one of the five by name — `-t todo` in an old script or tutorial —
|
|
18
|
+
is refused with a message that says the template was retired, instead of the
|
|
19
|
+
generic "Unknown template" error that reads as a typo.
|
|
20
|
+
- The GitHub tarball-fetch path that served the remote templates is removed
|
|
21
|
+
along with its `tar` dependency; nothing else reached it.
|
|
22
|
+
|
|
23
|
+
Note this corrects the catalog at HEAD only. Already-published versions keep
|
|
24
|
+
advertising the retired templates until a new version of `create-objectstack`
|
|
25
|
+
is released.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 4906c90: Fix scaffolded projects describing themselves as the blank template (#9263)
|
|
30
|
+
|
|
31
|
+
`rewriteProjectIdentity` rewrote `id` / `namespace` / `name` in both
|
|
32
|
+
`objectstack.config.ts` and `objectstack.manifest.json` from the project name,
|
|
33
|
+
but left `description` untouched — every scaffolded project carried the blank
|
|
34
|
+
template's own line verbatim ("Minimal ObjectStack environment — a clean
|
|
35
|
+
slate for building."), confidently wrong rather than empty, and printed by
|
|
36
|
+
the first command the getting-started flow tells people to run (`os
|
|
37
|
+
validate`).
|
|
38
|
+
|
|
39
|
+
The scaffolder now drops `description` from both files instead of rewriting
|
|
40
|
+
it. There is nothing but the project name to derive a replacement from, and a
|
|
41
|
+
name-derived sentence (e.g. "Support Desk — an ObjectStack environment.")
|
|
42
|
+
would be a bare restatement of the `name`/`displayName` row already shown —
|
|
43
|
+
worse than no sentence at all. `os validate` already omits the description
|
|
44
|
+
line entirely when the field is unset, so a freshly scaffolded project now
|
|
45
|
+
prints cleanly:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Support Desk v0.1.0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
instead of
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Support Desk v0.1.0
|
|
55
|
+
Minimal ObjectStack environment — a clean slate for building.
|
|
56
|
+
```
|
|
57
|
+
- f2f09e4: fix(create-objectstack): the scaffolded Dockerfile pins the runtime image to the CLI that builds the artifact, instead of `latest` under a comment saying to pin (#9017)
|
|
58
|
+
|
|
59
|
+
`src/templates/blank/Dockerfile` shipped `FROM ghcr.io/objectstack-ai/objectstack:latest`
|
|
60
|
+
directly beneath a comment instructing the reader to "pin the tag to the
|
|
61
|
+
`@objectstack/cli` version in your package.json so the runtime matches the CLI that built
|
|
62
|
+
the artifact" — an instruction the scaffold itself did not follow. Every app made with
|
|
63
|
+
`npx create-objectstack` shipped that contradiction from day one, and `docker/README.md`'s
|
|
64
|
+
tag table already scopes `latest` to quick starts while documenting `X.Y.Z` as the
|
|
65
|
+
production pin.
|
|
66
|
+
|
|
67
|
+
Measured on scaffolded output rather than the template's bytes, before the fix:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
emitted package.json cli range : ^17.0.0
|
|
71
|
+
emitted Dockerfile FROM : FROM ghcr.io/objectstack-ai/objectstack:latest
|
|
72
|
+
agreement (tag vs cli range) : DISAGREE
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**The tag is resolved after `install`, from the installed CLI — not from the generated
|
|
76
|
+
`package.json`.** That file carries a caret RANGE, and the two are not interchangeable:
|
|
77
|
+
npm resolves `^17.0.0` to the newest 17.x, so pinning the range's floor would ship a
|
|
78
|
+
runtime image *older* than the CLI that built the artifact — breaking the same promise in
|
|
79
|
+
a new way. The rolling `:17` tag does match the range's float window but is exactly what
|
|
80
|
+
the tag table tells production not to use. The resolved version is the only value that
|
|
81
|
+
makes the sentence true, and it is the rule the repo already applies for this purpose in
|
|
82
|
+
`.github/workflows/scaffold-e2e.yml` ("Pin the runtime's CLI to the SAME version the
|
|
83
|
+
generated project actually resolved to — NOT a hardcoded `latest`").
|
|
84
|
+
|
|
85
|
+
**Both halves move together.** Pinning the line while leaving an imperative to pin by hand
|
|
86
|
+
would relocate the contradiction rather than remove it, so the comment above the `FROM`
|
|
87
|
+
line is replaced in the same rewrite. With `--skip-install` there is no resolved version:
|
|
88
|
+
the tag stays `latest` and the comment keeps telling the reader to pin — which is true on
|
|
89
|
+
that path, because there the user really must do it by hand.
|
|
90
|
+
|
|
91
|
+
The regression proof asserts on **scaffolded output**, never on the template: it scaffolds
|
|
92
|
+
with the real copy/sync/pin path, plants an installed CLI whose version is deliberately
|
|
93
|
+
*not* the range's floor (the normal case, and the one that a package.json-derived tag
|
|
94
|
+
would get wrong), and checks the emitted `FROM` tag against the emitted `package.json`
|
|
95
|
+
range with a satisfies-check rather than equality.
|
|
96
|
+
|
|
97
|
+
`.github/workflows/scaffold-e2e.yml` now reads the tag it builds its local runtime image
|
|
98
|
+
under **out of the generated Dockerfile** instead of hardcoding `:latest`. Those were two
|
|
99
|
+
hand-matched literals; had they skewed, Docker would have quietly pulled the last
|
|
100
|
+
published image instead of the one built from this checkout, and the job's own stated
|
|
101
|
+
hermeticity would have been false while it stayed green.
|
|
102
|
+
- 0a5adba: fix(create-objectstack): the blank template's `specVersion` stops shipping eleven majors stale, and the version-time sync covers every declared surface on every template (#9264)
|
|
103
|
+
|
|
104
|
+
The one bundled template declared the platform it targets in **two** places that
|
|
105
|
+
disagreed by eleven majors:
|
|
106
|
+
|
|
107
|
+
| file | key | was |
|
|
108
|
+
|:--|:--|:--|
|
|
109
|
+
| `objectstack.manifest.json` | `specVersion` | `^6.0.0` |
|
|
110
|
+
| `objectstack.config.ts` | `engines.protocol` | `^17` |
|
|
111
|
+
|
|
112
|
+
`scripts/sync-template-versions.mjs` re-stamped the config key and the template's
|
|
113
|
+
`@objectstack/*` dependency ranges, and **never opened the manifest at all**. So
|
|
114
|
+
`engines.protocol` tracked every major bump while `specVersion` sat at the value
|
|
115
|
+
it held when the script was written — and a green `sync-template-versions` run
|
|
116
|
+
was never evidence about it, because the script's failure mode was loud for the
|
|
117
|
+
keys it covered and mute for the key it did not.
|
|
118
|
+
|
|
119
|
+
**This is not confined to the registry contract.** `create-objectstack` copies
|
|
120
|
+
the manifest into every scaffolded project, rewriting `name`, `displayName` and
|
|
121
|
+
`namespace` and dropping `description` — it has never touched `specVersion`. So
|
|
122
|
+
every project scaffolded since v7 was stamped with a `^6.0.0` spec range while
|
|
123
|
+
installing `@objectstack/spec@^17.0.0`.
|
|
124
|
+
|
|
125
|
+
**The two keys are two facts, and the fix keeps them apart.** `engines.protocol`
|
|
126
|
+
is the ADR-0087 D1 runtime handshake range and carries the protocol major
|
|
127
|
+
(`^17`). `specVersion` is documented by `TemplateManifestSchema` as the
|
|
128
|
+
"Compatible `@objectstack/spec` semver range" and carries the package range
|
|
129
|
+
(`^17.0.0`) — the same value the script already writes into the template's own
|
|
130
|
+
`@objectstack/spec` dependency, so the manifest and the `package.json` now state
|
|
131
|
+
one fact once. They agree on the major only because the spec package's major and
|
|
132
|
+
the protocol major are kept in lockstep; they are stamped from two different
|
|
133
|
+
values.
|
|
134
|
+
|
|
135
|
+
Deleting the key was not available: `specVersion` is **required** by
|
|
136
|
+
`TemplateManifestSchema`, and every shipped manifest is parsed against it by
|
|
137
|
+
`check:template-manifests`.
|
|
138
|
+
|
|
139
|
+
**Two structural changes, because one-key-one-file coverage is what let this
|
|
140
|
+
sit:**
|
|
141
|
+
|
|
142
|
+
- the sync script's file list is now **discovered**, not hard-coded — templates
|
|
143
|
+
are found by walking `src/templates/`, the same way `check-template-manifests`
|
|
144
|
+
finds the manifests it parses, so a second template is covered on the day it
|
|
145
|
+
lands;
|
|
146
|
+
- **every stamp is required**. A template whose file is missing, whose stamp is
|
|
147
|
+
absent, or whose `package.json` declares no `@objectstack/*` dependency is a
|
|
148
|
+
hard failure naming the path — never a skip. A skipped stamp is
|
|
149
|
+
indistinguishable from a synced one in the log, which is the invisibility this
|
|
150
|
+
fixes.
|
|
151
|
+
|
|
152
|
+
The manifest is rewritten as **text** rather than parsed and re-serialized:
|
|
153
|
+
`objectstack.manifest.json` keeps `scaffold.variables` compact on one line, and
|
|
154
|
+
`JSON.stringify(…, null, 2)` would reformat unrelated structure on every release.
|
|
155
|
+
|
|
156
|
+
CI coverage lands as four per-template ratchets in `template-consistency.test.ts`,
|
|
157
|
+
generalized off `blank` onto the same directory walk — including the invariant
|
|
158
|
+
that catches this exact class: the manifest's `specVersion` must equal the
|
|
159
|
+
`@objectstack/spec` range the template actually installs. Either file alone can
|
|
160
|
+
be self-consistently stale; only comparing them catches a stamp that covered one
|
|
161
|
+
and not the other.
|
|
162
|
+
|
|
163
|
+
## 17.0.0
|
|
164
|
+
|
|
165
|
+
### Major Changes
|
|
166
|
+
|
|
167
|
+
- e47b342: feat!: require Node.js 22 — promise the runtime we actually test (#3825)
|
|
168
|
+
|
|
169
|
+
Every published package declared `engines.node: ">=18.0.0"`. **Node 18 reached
|
|
170
|
+
end-of-life on 2025-04-30 and Node 20 on 2026-04-30**, so the compatibility
|
|
171
|
+
promise covered two runtimes nobody patches — and, after #3830 moved CI to Node
|
|
172
|
+
22, two runtimes nothing in this repo verifies.
|
|
173
|
+
|
|
174
|
+
That left the promise and the evidence with **no overlap at all**:
|
|
175
|
+
|
|
176
|
+
| | Node version |
|
|
177
|
+
| ----------------------------------------------------------------------------------------------- | ------------ |
|
|
178
|
+
| What CI validates every PR on | **22** |
|
|
179
|
+
| What `release.yml` publishes from | **22** |
|
|
180
|
+
| What every shipped Docker image runs (`docker/Dockerfile`, `blank` template, self-hosting docs) | **22** |
|
|
181
|
+
| What `engines.node` promised users | **>=18** |
|
|
182
|
+
|
|
183
|
+
`engines.node` is now `>=22.0.0` across all 50 manifests. This is the honest
|
|
184
|
+
floor: it is the only runtime the packages are built, tested and shipped on.
|
|
185
|
+
|
|
186
|
+
## Migration
|
|
187
|
+
|
|
188
|
+
**If you are on Node 22 or newer, nothing changes.** Node 24 (Active LTS since
|
|
189
|
+
2025-10-28) and Node 26 both satisfy the new range.
|
|
190
|
+
|
|
191
|
+
If you are on Node 18 or 20, upgrade to Node 22+. Both are past end-of-life and
|
|
192
|
+
receive no security patches:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
nvm install 22 && nvm use 22
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
npm and pnpm surface an unsatisfied `engines` as an **`EBADENGINE` warning**, not
|
|
199
|
+
a hard failure, so an existing install will not break the moment you upgrade —
|
|
200
|
+
but the package is no longer tested on that runtime, and the failures are the
|
|
201
|
+
kind that do not announce themselves. #3812 is the worked example: a native
|
|
202
|
+
dependency whose `engines` required a newer Node loaded anyway on the older one
|
|
203
|
+
and then killed the test worker at the process level, with no JS error and a
|
|
204
|
+
summary that still said "passed".
|
|
205
|
+
|
|
206
|
+
If your CI pins Node, pin it to 22 as well — running your gates on a runtime
|
|
207
|
+
your dependencies no longer support is exactly the split this change closes.
|
|
208
|
+
|
|
209
|
+
## Also updated
|
|
210
|
+
|
|
211
|
+
The "Node 18+" prerequisite was restated in ten user-facing places
|
|
212
|
+
(`README.md`, `CONTRIBUTING.md`, the getting-started and deployment docs, the
|
|
213
|
+
todo example, and the `objectstack-platform` skill's `compatibility` field).
|
|
214
|
+
All now say 22. Changelogs and ADRs are historical records and were left alone.
|
|
215
|
+
|
|
216
|
+
### Patch Changes
|
|
217
|
+
|
|
218
|
+
- 9f060e5: chore(deps)!: better-auth 1.7.0-rc.2 (account identity restructuring) + the
|
|
219
|
+
production-dependency batch from #3517
|
|
220
|
+
|
|
221
|
+
**better-auth 1.7.0-rc.1 → 1.7.0-rc.2** across the family (`better-auth`,
|
|
222
|
+
`@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/sso`, and the
|
|
223
|
+
adapter/telemetry overrides). `@better-auth/scim` deliberately stays on
|
|
224
|
+
1.7.0-rc.1 — rc.2 replaces its whole model (code-defined connections; the
|
|
225
|
+
`scimProvider` model and the generate-token endpoint are gone), which is a
|
|
226
|
+
feature migration, not a version bump. Its peer range accepts rc.2 core, and the
|
|
227
|
+
advisory that forced the original pin (GHSA-j8v8-g9cx-5qf4) is still fixed.
|
|
228
|
+
|
|
229
|
+
**BREAKING — account identity.** better-auth renamed `account.accountId` to
|
|
230
|
+
`account.providerAccountId` and added a REQUIRED `account.issuer`; sign-in now
|
|
231
|
+
resolves accounts by `(issuer, providerAccountId)`.
|
|
232
|
+
|
|
233
|
+
- FROM `fields: { accountId: 'account_id' }` → TO
|
|
234
|
+
`fields: { issuer: 'issuer', providerAccountId: 'account_id' }`. The provider
|
|
235
|
+
account id keeps its `account_id` column — only the better-auth-side name
|
|
236
|
+
moved — and `sys_account` gains an `issuer` column.
|
|
237
|
+
- FROM `internalAdapter.createAccount({ providerId, accountId, … })` → TO
|
|
238
|
+
`createAccount({ providerId, issuer, providerAccountId, … })`. A local
|
|
239
|
+
password account carries the issuer better-auth mints for itself,
|
|
240
|
+
`local:credential`.
|
|
241
|
+
- FROM `client.auth.accounts.unlink({ providerId, accountId })` → TO
|
|
242
|
+
`unlink({ accountId })`, where `accountId` is now the account ROW id (the `id`
|
|
243
|
+
from `accounts.list()`), matching better-auth's narrowed body.
|
|
244
|
+
`accounts.list()` returns `issuer` + `providerAccountId` in place of
|
|
245
|
+
`accountId`.
|
|
246
|
+
|
|
247
|
+
**Existing deployments:** rows written before 1.7 have no issuer and are
|
|
248
|
+
invisible to sign-in until stamped. The auth plugin now runs an idempotent
|
|
249
|
+
boot-time backfill that stamps what it can derive — `local:credential` for
|
|
250
|
+
password accounts, `local:oauth:<providerId>` for configured social providers,
|
|
251
|
+
and the registered IdP's real `iss` from `sys_sso_provider` for federated ones.
|
|
252
|
+
Accounts from a federated IdP that is no longer registered cannot be derived;
|
|
253
|
+
they are logged with their provider id and row count rather than guessed, and
|
|
254
|
+
those users cannot sign in through that provider until the row is stamped with
|
|
255
|
+
the IdP's issuer or removed so a fresh login re-links it.
|
|
256
|
+
|
|
257
|
+
**Also required by 1.7:** `SecondaryStorage` gained two mandatory methods, both
|
|
258
|
+
now implemented over the kernel cache service — `getAndDelete` (single-use
|
|
259
|
+
verification values) and `increment` (fixed-window rate-limit counter;
|
|
260
|
+
`rateLimit.storage: 'secondary-storage'` throws at boot without it).
|
|
261
|
+
|
|
262
|
+
The rest of #3517's production-dependency batch rides along: `@oclif/core`
|
|
263
|
+
4.13.0, `@hono/node-server` 2.0.12, `hono` 4.12.32, `tar` 7.5.22, `jose` 6.2.4,
|
|
264
|
+
`pinyin-pro` 3.28.2, plus the private docs app's fumadocs/next/react bumps.
|
|
265
|
+
|
|
266
|
+
- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
|
|
267
|
+
|
|
268
|
+
The AGENTS.md post-task checklist requires breaking changesets to carry their
|
|
269
|
+
FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
|
|
270
|
+
inside the npm package and is what an upgrading agent greps after the tombstone
|
|
271
|
+
error." That delivery path was severed for 68 of the 69 publishable packages:
|
|
272
|
+
npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
|
|
273
|
+
older npm versions — not `CHANGELOG.md`, and the canonical
|
|
274
|
+
`"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
|
|
275
|
+
10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
|
|
276
|
+
70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
|
|
277
|
+
explicitly.
|
|
278
|
+
|
|
279
|
+
The tombstone-error scenario is precisely the one where the repo is out of
|
|
280
|
+
reach — the upgrading agent has `node_modules` and nothing else — so the
|
|
281
|
+
migration text has to ride in the tarball. Every publishable package now
|
|
282
|
+
declares `CHANGELOG.md` in `files`, and the canonical whitelist is
|
|
283
|
+
`["dist", "README.md", "CHANGELOG.md"]`.
|
|
284
|
+
|
|
285
|
+
The other half is the gate: `check:published-files` gains a fifth invariant,
|
|
286
|
+
COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
|
|
287
|
+
always-required lint job, so the next package cannot silently sever the path
|
|
288
|
+
again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
|
|
289
|
+
into the canonical set.
|
|
290
|
+
|
|
291
|
+
Consumer-visible change: one more file per install (the package's changelog,
|
|
292
|
+
e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
|
|
293
|
+
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
|
|
294
|
+
promised.
|
|
295
|
+
|
|
296
|
+
- 4e9e184: chore(deps): OSV security batch — bump tar to ^7.5.21 (GHSA-r292-9mhp-454m) and
|
|
297
|
+
js-yaml to ^5.2.2 (GHSA-pm4m-ph32-ghv5)
|
|
298
|
+
|
|
299
|
+
Both are declared-range bumps to the patched releases, so downstream installs
|
|
300
|
+
resolve the fixed versions from the published manifests, not just this
|
|
301
|
+
workspace's lockfile. The same batch clears the remaining transitive advisories
|
|
302
|
+
(next 16.2.11 in apps/docs; workspace overrides for brace-expansion, sharp,
|
|
303
|
+
react-router, @sveltejs/kit, @hono/node-server) — those live in pnpm-workspace.yaml
|
|
304
|
+
and the private docs app, which do not ship.
|
|
305
|
+
|
|
306
|
+
- 8d41998: fix(create-objectstack): scaffolding a remote template no longer produces a project that cannot build (#4926)
|
|
307
|
+
|
|
308
|
+
`npx create-objectstack@latest my-app -t todo` (and `compliance`, `content`,
|
|
309
|
+
`contracts`, `procurement`) generated a project that failed `objectstack build`
|
|
310
|
+
immediately — 5 of the 6 offered templates. Only the bundled `blank` worked.
|
|
311
|
+
|
|
312
|
+
The scaffolder read the template's original namespace from
|
|
313
|
+
`objectstack.manifest.json`, and that filename names two different documents.
|
|
314
|
+
The bundled template's is app-shaped and carries `namespace`; a remote
|
|
315
|
+
template's is the template-registry document
|
|
316
|
+
(`$schema: …/template-manifest.json`) and carries none — its namespace lives
|
|
317
|
+
only in `objectstack.config.ts`. So the value came back `undefined` for every
|
|
318
|
+
remote template and the object-name rewrite was skipped, while the config's
|
|
319
|
+
`namespace:` was rewritten anyway. The result was `namespace: 'my_app'` sitting
|
|
320
|
+
next to `name: 'todo_task'`, which the `${namespace}_${shortName}` rule rejects.
|
|
321
|
+
Across the five templates, 74 object names were left unrewritten.
|
|
322
|
+
|
|
323
|
+
`objectstack.config.ts` is now the authority for the template namespace (it
|
|
324
|
+
holds the very literal the scaffolder overwrites, so the two cannot disagree),
|
|
325
|
+
with the manifest as fallback. The rewrite also verifies itself: any surviving
|
|
326
|
+
stale prefix throws at the scaffold, naming the files and lines, instead of
|
|
327
|
+
surfacing as a build failure on the user's first command.
|
|
328
|
+
|
|
329
|
+
- 7309c81: chore(cli,create-objectstack): scaffolds no longer name a driver (#4065)
|
|
330
|
+
|
|
331
|
+
`os init` and the `create-objectstack` blank template both listed
|
|
332
|
+
`@objectstack/driver-memory` in the generated `dependencies`. It was the only
|
|
333
|
+
driver named, which read as an endorsement — "this is the driver your app runs
|
|
334
|
+
on" — when it is in fact the **last-resort rung** of the dev step-down (native
|
|
335
|
+
`better-sqlite3` → WASM SQLite → mingo). A new project's first impression of the
|
|
336
|
+
data layer should not be the engine that enforces no primary keys, no
|
|
337
|
+
uniqueness, no `NOT NULL` and no column types.
|
|
338
|
+
|
|
339
|
+
It was also redundant: `@objectstack/runtime` already depends on `driver-sql`,
|
|
340
|
+
`driver-sqlite-wasm` and `driver-memory`, and every script in both scaffolds runs
|
|
341
|
+
through the CLI, which carries all four. Removing the line changes nothing a
|
|
342
|
+
generated project can do — `objectstack dev` still resolves SQLite by default,
|
|
343
|
+
and `OS_DATABASE_URL` still selects Postgres / MySQL / MongoDB.
|
|
344
|
+
|
|
345
|
+
Docs updated to match: the "packages you depend on" table in _Your first project_
|
|
346
|
+
no longer lists a driver row (it now says where drivers come from), and the
|
|
347
|
+
Memory Driver section of _Database Drivers_ documents the opt-in persistence
|
|
348
|
+
default, carries a migration callout for the old `'auto'` behaviour, and points
|
|
349
|
+
test authors at in-memory SQLite. That section also claimed "Data is lost when
|
|
350
|
+
the process exits", which was simply false while `'auto'` was the default — it
|
|
351
|
+
wrote a file into the working directory.
|
|
352
|
+
|
|
3
353
|
## 17.0.0-rc.6
|
|
4
354
|
|
|
5
355
|
## 17.0.0-rc.5
|
package/README.md
CHANGED
|
@@ -27,16 +27,14 @@ npx create-objectstack my-app --skip-install
|
|
|
27
27
|
| Template | Source | Description |
|
|
28
28
|
| --- | --- | --- |
|
|
29
29
|
| `blank` *(default)* | bundled (offline) | Minimal starter — one object, REST API, ready to extend |
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
scaffold time and require network access; `blank` is bundled and always works
|
|
39
|
-
offline.
|
|
30
|
+
|
|
31
|
+
`blank` is bundled inside the npm package, so scaffolding never needs network
|
|
32
|
+
access.
|
|
33
|
+
|
|
34
|
+
The remote content templates (`todo`, `compliance`, `content`, `contracts`,
|
|
35
|
+
`procurement`) have been **retired** — they were delisted from the ObjectStack
|
|
36
|
+
template marketplace and are no longer maintained. Asking for one by name tells
|
|
37
|
+
you so rather than failing as an unknown template.
|
|
40
38
|
|
|
41
39
|
## Options
|
|
42
40
|
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import os from "os";
|
|
4
|
+
import fs4 from "fs";
|
|
5
|
+
import path4 from "path";
|
|
7
6
|
import { execSync } from "child_process";
|
|
8
7
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { pipeline } from "stream/promises";
|
|
10
|
-
import { createGunzip } from "zlib";
|
|
11
|
-
import { createWriteStream, createReadStream } from "fs";
|
|
12
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
13
|
-
import * as tar from "tar";
|
|
14
8
|
|
|
15
9
|
// src/pkg-utils.ts
|
|
16
10
|
function syncObjectStackDeps(pkg, version) {
|
|
@@ -114,39 +108,94 @@ function findStaleNamespacePrefixes(dir, oldNs) {
|
|
|
114
108
|
return stale;
|
|
115
109
|
}
|
|
116
110
|
|
|
117
|
-
// src/
|
|
118
|
-
var __filename2 = fileURLToPath(import.meta.url);
|
|
119
|
-
var __dirname2 = path3.dirname(__filename2);
|
|
120
|
-
var BUNDLED_TEMPLATES_DIR = path3.resolve(__dirname2, "templates");
|
|
121
|
-
var REMOTE_REPO = "objectstack-ai/templates";
|
|
122
|
-
var REMOTE_BRANCH = "main";
|
|
123
|
-
var REMOTE_TARBALL_URL = `https://codeload.github.com/${REMOTE_REPO}/tar.gz/refs/heads/${REMOTE_BRANCH}`;
|
|
111
|
+
// src/template-registry.ts
|
|
124
112
|
var TEMPLATES = {
|
|
125
113
|
blank: {
|
|
126
114
|
description: "Minimal starter \u2014 one object, REST API, ready to extend",
|
|
127
115
|
source: { kind: "bundled", dir: "blank" }
|
|
128
|
-
},
|
|
129
|
-
todo: {
|
|
130
|
-
description: "Universal task & project management starter",
|
|
131
|
-
source: { kind: "remote", pkg: "todo" }
|
|
132
|
-
},
|
|
133
|
-
compliance: {
|
|
134
|
-
description: "Compliance posture & evidence management (SOC2 / ISO27001)",
|
|
135
|
-
source: { kind: "remote", pkg: "compliance" }
|
|
136
|
-
},
|
|
137
|
-
content: {
|
|
138
|
-
description: "Content marketing pipeline \u2014 editorial calendar & channel ROI",
|
|
139
|
-
source: { kind: "remote", pkg: "content" }
|
|
140
|
-
},
|
|
141
|
-
contracts: {
|
|
142
|
-
description: "Post-signature CLM \u2014 approvals, obligations, renewals",
|
|
143
|
-
source: { kind: "remote", pkg: "contracts" }
|
|
144
|
-
},
|
|
145
|
-
procurement: {
|
|
146
|
-
description: "Source-to-pay \u2014 vendors, POs, receipts, invoice matching",
|
|
147
|
-
source: { kind: "remote", pkg: "procurement" }
|
|
148
116
|
}
|
|
149
117
|
};
|
|
118
|
+
var RETIRED_TEMPLATES = [
|
|
119
|
+
"todo",
|
|
120
|
+
"compliance",
|
|
121
|
+
"content",
|
|
122
|
+
"contracts",
|
|
123
|
+
"procurement"
|
|
124
|
+
];
|
|
125
|
+
function lookupTemplate(name) {
|
|
126
|
+
const template = TEMPLATES[name];
|
|
127
|
+
if (template) return { kind: "found", name, template };
|
|
128
|
+
if (RETIRED_TEMPLATES.includes(name)) return { kind: "retired", name };
|
|
129
|
+
return { kind: "unknown", name };
|
|
130
|
+
}
|
|
131
|
+
function templateNames() {
|
|
132
|
+
return Object.keys(TEMPLATES);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/runtime-image.ts
|
|
136
|
+
import fs3 from "fs";
|
|
137
|
+
import path3 from "path";
|
|
138
|
+
var RUNTIME_FROM_RE = /^FROM ghcr\.io\/objectstack-ai\/objectstack:([A-Za-z0-9_][A-Za-z0-9_.+-]*)[ \t]*$/;
|
|
139
|
+
var PINNABLE_VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
140
|
+
var PROSE_COMMENT_RE = /^#[ \t]+\S/;
|
|
141
|
+
function readResolvedCliVersion(targetDir) {
|
|
142
|
+
const pkgPath = path3.join(
|
|
143
|
+
targetDir,
|
|
144
|
+
"node_modules",
|
|
145
|
+
"@objectstack",
|
|
146
|
+
"cli",
|
|
147
|
+
"package.json"
|
|
148
|
+
);
|
|
149
|
+
try {
|
|
150
|
+
const version = JSON.parse(fs3.readFileSync(pkgPath, "utf8")).version;
|
|
151
|
+
return typeof version === "string" && PINNABLE_VERSION_RE.test(version) ? version : void 0;
|
|
152
|
+
} catch {
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function pinnedComment(version) {
|
|
157
|
+
return [
|
|
158
|
+
"# Pinned at scaffold time to the @objectstack/cli version this project",
|
|
159
|
+
"# resolved, so the runtime runs the same CLI that built your artifact.",
|
|
160
|
+
"# Move both together when you upgrade \u2014 see docker/README.md tag table.",
|
|
161
|
+
`FROM ghcr.io/objectstack-ai/objectstack:${version}`
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
function pinRuntimeImage(targetDir, version) {
|
|
165
|
+
if (!PINNABLE_VERSION_RE.test(version)) {
|
|
166
|
+
return { pinned: false, reason: `'${version}' is not a pinnable version` };
|
|
167
|
+
}
|
|
168
|
+
const dockerfile = path3.join(targetDir, "Dockerfile");
|
|
169
|
+
let text;
|
|
170
|
+
try {
|
|
171
|
+
text = fs3.readFileSync(dockerfile, "utf8");
|
|
172
|
+
} catch {
|
|
173
|
+
return { pinned: false, reason: "no Dockerfile in the scaffolded project" };
|
|
174
|
+
}
|
|
175
|
+
const lines = text.split("\n");
|
|
176
|
+
const fromIdx = lines.findIndex((line) => RUNTIME_FROM_RE.test(line));
|
|
177
|
+
if (fromIdx === -1) {
|
|
178
|
+
return {
|
|
179
|
+
pinned: false,
|
|
180
|
+
reason: "no `FROM ghcr.io/objectstack-ai/objectstack:<tag>` line found"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
let start = fromIdx;
|
|
184
|
+
while (start > 0 && PROSE_COMMENT_RE.test(lines[start - 1])) start--;
|
|
185
|
+
lines.splice(start, fromIdx - start + 1, ...pinnedComment(version));
|
|
186
|
+
const pinnedText = lines.join("\n");
|
|
187
|
+
const check = pinnedText.split("\n").map((line) => RUNTIME_FROM_RE.exec(line)).find((match) => match !== null);
|
|
188
|
+
if (!check || check[1] !== version) {
|
|
189
|
+
return { pinned: false, reason: "rewrite did not produce the pinned tag" };
|
|
190
|
+
}
|
|
191
|
+
fs3.writeFileSync(dockerfile, pinnedText);
|
|
192
|
+
return { pinned: true, tag: version };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/index.ts
|
|
196
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
197
|
+
var __dirname2 = path4.dirname(__filename2);
|
|
198
|
+
var BUNDLED_TEMPLATES_DIR = path4.resolve(__dirname2, "templates");
|
|
150
199
|
function toTitleCase(str) {
|
|
151
200
|
return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
152
201
|
}
|
|
@@ -163,8 +212,8 @@ function sanitizeNamespace(name) {
|
|
|
163
212
|
}
|
|
164
213
|
function readCliVersion() {
|
|
165
214
|
try {
|
|
166
|
-
const pkgPath =
|
|
167
|
-
const pkg = JSON.parse(
|
|
215
|
+
const pkgPath = path4.resolve(__dirname2, "..", "package.json");
|
|
216
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
|
|
168
217
|
return String(pkg.version || "0.0.0");
|
|
169
218
|
} catch {
|
|
170
219
|
return "0.0.0";
|
|
@@ -199,98 +248,50 @@ function detectPackageManager() {
|
|
|
199
248
|
}
|
|
200
249
|
}
|
|
201
250
|
function loadBundled(templateDir, targetDir) {
|
|
202
|
-
const src =
|
|
203
|
-
if (!
|
|
251
|
+
const src = path4.join(BUNDLED_TEMPLATES_DIR, templateDir);
|
|
252
|
+
if (!fs4.existsSync(src)) {
|
|
204
253
|
throw new Error(`Bundled template missing on disk: ${src}`);
|
|
205
254
|
}
|
|
206
255
|
const collected = [];
|
|
207
256
|
copyDir(src, targetDir, collected);
|
|
208
257
|
return collected;
|
|
209
258
|
}
|
|
210
|
-
async function downloadTarball(url, destFile) {
|
|
211
|
-
const res = await fetch(url, { redirect: "follow" });
|
|
212
|
-
if (!res.ok || !res.body) {
|
|
213
|
-
throw new Error(`Download failed: ${url} (${res.status})`);
|
|
214
|
-
}
|
|
215
|
-
const out = createWriteStream(destFile);
|
|
216
|
-
for await (const chunk of res.body) {
|
|
217
|
-
out.write(chunk);
|
|
218
|
-
}
|
|
219
|
-
await new Promise((resolve, reject) => {
|
|
220
|
-
out.end((err) => err ? reject(err) : resolve());
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
async function loadRemote(pkgName, targetDir) {
|
|
224
|
-
const tmp = await mkdtemp(path3.join(os.tmpdir(), "create-objectstack-"));
|
|
225
|
-
try {
|
|
226
|
-
const tarball = path3.join(tmp, "templates.tar.gz");
|
|
227
|
-
printStep(`Fetching template "${pkgName}" from ${REMOTE_REPO}@${REMOTE_BRANCH}\u2026`);
|
|
228
|
-
await downloadTarball(REMOTE_TARBALL_URL, tarball);
|
|
229
|
-
fs3.mkdirSync(targetDir, { recursive: true });
|
|
230
|
-
const collected = [];
|
|
231
|
-
await pipeline(
|
|
232
|
-
createReadStream(tarball),
|
|
233
|
-
createGunzip(),
|
|
234
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
235
|
-
tar.extract({
|
|
236
|
-
cwd: targetDir,
|
|
237
|
-
strip: 3,
|
|
238
|
-
filter: (p) => {
|
|
239
|
-
const parts = p.split("/");
|
|
240
|
-
return parts[1] === "packages" && parts[2] === pkgName && parts.length > 3;
|
|
241
|
-
},
|
|
242
|
-
onentry: (entry) => {
|
|
243
|
-
if (entry.type === "File") {
|
|
244
|
-
const parts = entry.path.split("/").slice(3);
|
|
245
|
-
if (parts.length > 0) collected.push(parts.join("/"));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
})
|
|
249
|
-
);
|
|
250
|
-
if (collected.length === 0) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
`Template "${pkgName}" not found in ${REMOTE_REPO}@${REMOTE_BRANCH} (expected packages/${pkgName}/).`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
return collected;
|
|
256
|
-
} finally {
|
|
257
|
-
await rm(tmp, { recursive: true, force: true });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
259
|
function rewriteProjectIdentity(targetDir, projectName, namespace) {
|
|
261
260
|
const title = toTitleCase(projectName);
|
|
262
261
|
const templateNamespace = readTemplateNamespace(targetDir);
|
|
263
|
-
const pkgPath =
|
|
264
|
-
if (
|
|
262
|
+
const pkgPath = path4.join(targetDir, "package.json");
|
|
263
|
+
if (fs4.existsSync(pkgPath)) {
|
|
265
264
|
try {
|
|
266
|
-
const pkg = JSON.parse(
|
|
265
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
|
|
267
266
|
pkg.name = projectName;
|
|
268
267
|
syncObjectStackDeps(pkg, readCliVersion());
|
|
269
|
-
|
|
268
|
+
fs4.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
270
269
|
} catch {
|
|
271
270
|
}
|
|
272
271
|
}
|
|
273
|
-
const manifestPath =
|
|
274
|
-
if (
|
|
272
|
+
const manifestPath = path4.join(targetDir, "objectstack.manifest.json");
|
|
273
|
+
if (fs4.existsSync(manifestPath)) {
|
|
275
274
|
try {
|
|
276
|
-
const m = JSON.parse(
|
|
275
|
+
const m = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
|
|
277
276
|
m.name = projectName;
|
|
278
277
|
m.displayName = title;
|
|
279
278
|
if ("namespace" in m) m.namespace = namespace;
|
|
280
|
-
|
|
279
|
+
delete m.description;
|
|
280
|
+
fs4.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
281
281
|
} catch {
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
|
-
const configPath =
|
|
285
|
-
if (
|
|
286
|
-
let cfg =
|
|
284
|
+
const configPath = path4.join(targetDir, "objectstack.config.ts");
|
|
285
|
+
if (fs4.existsSync(configPath)) {
|
|
286
|
+
let cfg = fs4.readFileSync(configPath, "utf8");
|
|
287
287
|
cfg = cfg.replace(/(\bid:\s*)(['"`])[^'"`]*\2/, `$1$2${projectName}$2`);
|
|
288
288
|
cfg = cfg.replace(/(\bnamespace:\s*)(['"`])[^'"`]*\2/, `$1$2${namespace}$2`);
|
|
289
289
|
cfg = cfg.replace(/(\bname:\s*)(['"`])[^'"`]*\2/, `$1$2${title}$2`);
|
|
290
|
-
|
|
290
|
+
cfg = cfg.replace(/^[ \t]*description:\s*(['"`])[^'"`]*\1,?\r?\n/m, "");
|
|
291
|
+
fs4.writeFileSync(configPath, cfg);
|
|
291
292
|
}
|
|
292
293
|
if (templateNamespace && namespace !== templateNamespace) {
|
|
293
|
-
const srcDir =
|
|
294
|
+
const srcDir = path4.join(targetDir, "src");
|
|
294
295
|
rewriteObjectNamePrefix(srcDir, templateNamespace, namespace);
|
|
295
296
|
const stale = findStaleNamespacePrefixes(srcDir, templateNamespace);
|
|
296
297
|
if (stale.length > 0) {
|
|
@@ -304,77 +305,82 @@ The generated project would fail 'objectstack build' on the \${namespace}_\${sho
|
|
|
304
305
|
);
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
|
-
const readmePath =
|
|
308
|
-
if (
|
|
309
|
-
let md =
|
|
308
|
+
const readmePath = path4.join(targetDir, "README.md");
|
|
309
|
+
if (fs4.existsSync(readmePath)) {
|
|
310
|
+
let md = fs4.readFileSync(readmePath, "utf8");
|
|
310
311
|
md = md.replace(/^#\s+.*$/m, `# ${title}`);
|
|
311
|
-
|
|
312
|
+
fs4.writeFileSync(readmePath, md);
|
|
312
313
|
}
|
|
313
314
|
writeAgentGuides(targetDir, title, projectName);
|
|
314
315
|
}
|
|
315
316
|
function writeAgentGuides(targetDir, title, projectName) {
|
|
316
|
-
const templatePath =
|
|
317
|
+
const templatePath = path4.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
|
|
317
318
|
let template;
|
|
318
319
|
try {
|
|
319
|
-
template =
|
|
320
|
+
template = fs4.readFileSync(templatePath, "utf8");
|
|
320
321
|
} catch (err) {
|
|
321
322
|
if (err?.code === "ENOENT") return;
|
|
322
323
|
throw err;
|
|
323
324
|
}
|
|
324
325
|
const rendered = template.replace(/\{\{PROJECT_TITLE\}\}/g, title).replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
325
|
-
writeIfAbsent(
|
|
326
|
-
const copilotPath =
|
|
327
|
-
|
|
326
|
+
writeIfAbsent(path4.join(targetDir, "AGENTS.md"), rendered);
|
|
327
|
+
const copilotPath = path4.join(targetDir, ".github", "copilot-instructions.md");
|
|
328
|
+
fs4.mkdirSync(path4.dirname(copilotPath), { recursive: true });
|
|
328
329
|
writeIfAbsent(copilotPath, rendered);
|
|
329
330
|
}
|
|
330
331
|
function writeIfAbsent(filePath, contents) {
|
|
331
332
|
try {
|
|
332
|
-
|
|
333
|
+
fs4.writeFileSync(filePath, contents, { flag: "wx" });
|
|
333
334
|
} catch (err) {
|
|
334
335
|
if (err?.code !== "EEXIST") throw err;
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
var program = new Command().name("create-objectstack").description("Create a new ObjectStack environment").version(readCliVersion()).argument("[name]", "Environment name (defaults to current directory name)").option(
|
|
338
339
|
"-t, --template <template>",
|
|
339
|
-
`Template: ${
|
|
340
|
+
`Template: ${templateNames().join(", ")}`,
|
|
340
341
|
"blank"
|
|
341
|
-
).option("--skip-install", "Skip dependency installation").option("--skip-skills", "Skip installing ObjectStack AI skills").action(
|
|
342
|
+
).option("--skip-install", "Skip dependency installation").option("--skip-skills", "Skip installing ObjectStack AI skills").action((name, options) => {
|
|
342
343
|
console.log("");
|
|
343
344
|
console.log(chalk.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
344
345
|
console.log(chalk.bold.cyan(" \u2551") + chalk.bold(" \u25C6 Create ObjectStack ") + chalk.dim("v6.x") + chalk.bold.cyan(" \u2551"));
|
|
345
346
|
console.log(chalk.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
346
347
|
printHeader("New Environment");
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
348
|
+
const lookup = lookupTemplate(options.template);
|
|
349
|
+
if (lookup.kind !== "found") {
|
|
350
|
+
if (lookup.kind === "retired") {
|
|
351
|
+
printError(`Template "${lookup.name}" has been retired and is no longer available.`);
|
|
352
|
+
console.log(
|
|
353
|
+
chalk.dim(
|
|
354
|
+
" It was delisted from the ObjectStack template marketplace and is no longer maintained."
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
printError(`Unknown template: ${lookup.name}`);
|
|
359
|
+
}
|
|
360
|
+
console.log(chalk.dim(` Available: ${templateNames().join(", ")}`));
|
|
351
361
|
process.exit(1);
|
|
352
362
|
}
|
|
363
|
+
const template = lookup.template;
|
|
353
364
|
const cwd = process.cwd();
|
|
354
|
-
const projectName = name ||
|
|
365
|
+
const projectName = name || path4.basename(cwd);
|
|
355
366
|
const namespace = sanitizeNamespace(projectName);
|
|
356
|
-
const targetDir = name ?
|
|
367
|
+
const targetDir = name ? path4.resolve(cwd, name) : cwd;
|
|
357
368
|
const isCurrentDir = targetDir === cwd;
|
|
358
369
|
printKV("Environment", projectName);
|
|
359
370
|
printKV("Namespace", namespace);
|
|
360
371
|
printKV("Template", `${options.template} \u2014 ${template.description}`);
|
|
361
372
|
printKV("Directory", targetDir);
|
|
362
373
|
console.log("");
|
|
363
|
-
if (!isCurrentDir &&
|
|
364
|
-
const existing =
|
|
374
|
+
if (!isCurrentDir && fs4.existsSync(targetDir)) {
|
|
375
|
+
const existing = fs4.readdirSync(targetDir);
|
|
365
376
|
if (existing.length > 0) {
|
|
366
377
|
printError(`Directory already exists and is not empty: ${targetDir}`);
|
|
367
378
|
process.exit(1);
|
|
368
379
|
}
|
|
369
380
|
}
|
|
370
381
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (template.source.kind === "bundled") {
|
|
374
|
-
createdFiles = loadBundled(template.source.dir, targetDir);
|
|
375
|
-
} else {
|
|
376
|
-
createdFiles = await loadRemote(template.source.pkg, targetDir);
|
|
377
|
-
}
|
|
382
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
383
|
+
const createdFiles = loadBundled(template.source.dir, targetDir);
|
|
378
384
|
rewriteProjectIdentity(targetDir, projectName, namespace);
|
|
379
385
|
console.log(chalk.bold(" Created files:"));
|
|
380
386
|
for (const f of createdFiles.slice(0, 20)) {
|
|
@@ -386,14 +392,30 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
386
392
|
console.log("");
|
|
387
393
|
if (!options.skipInstall) {
|
|
388
394
|
printStep("Installing dependencies...");
|
|
395
|
+
let installed = false;
|
|
389
396
|
try {
|
|
390
397
|
const pm = detectPackageManager();
|
|
391
398
|
execSync(`${pm} install`, { stdio: "inherit", cwd: targetDir });
|
|
399
|
+
installed = true;
|
|
392
400
|
console.log("");
|
|
393
401
|
} catch {
|
|
394
402
|
printWarning("Dependency installation failed. Run `npm install` manually.");
|
|
395
403
|
console.log("");
|
|
396
404
|
}
|
|
405
|
+
if (installed) {
|
|
406
|
+
const resolved = readResolvedCliVersion(targetDir);
|
|
407
|
+
if (resolved) {
|
|
408
|
+
const result = pinRuntimeImage(targetDir, resolved);
|
|
409
|
+
if (result.pinned) {
|
|
410
|
+
printSuccess(`Dockerfile runtime image pinned to ${result.tag}`);
|
|
411
|
+
} else {
|
|
412
|
+
printWarning(
|
|
413
|
+
`Could not pin the Dockerfile runtime image (${result.reason}); it still reads \`latest\` \u2014 pin it to ${resolved} before deploying.`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
console.log("");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
397
419
|
}
|
|
398
420
|
if (!options.skipInstall && !options.skipSkills) {
|
|
399
421
|
printStep("Installing AI skills for your coding agent...");
|
|
@@ -20,9 +20,12 @@ RUN npx os build # → dist/objectstack.json
|
|
|
20
20
|
# ── Runtime: the official ObjectStack runtime image ──────────────────
|
|
21
21
|
# Ships Node + @objectstack/cli with `os start`, a non-root user, the
|
|
22
22
|
# /api/v1/health HEALTHCHECK, and OS_ARTIFACT_PATH/OS_PORT preset (port 8080)
|
|
23
|
-
# — see docker/README.md in the framework repo.
|
|
24
|
-
#
|
|
25
|
-
#
|
|
23
|
+
# — see docker/README.md in the framework repo.
|
|
24
|
+
#
|
|
25
|
+
# Dependencies were not installed while scaffolding, so the tag below could
|
|
26
|
+
# not be resolved for you. `latest` floats to whatever release is newest,
|
|
27
|
+
# which is unrelated to the @objectstack/cli your app resolves — pin it to
|
|
28
|
+
# that version, the CLI that builds your artifact, before you deploy.
|
|
26
29
|
FROM ghcr.io/objectstack-ai/objectstack:latest
|
|
27
30
|
COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json
|
|
28
31
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://schemas.objectstack.dev/template-manifest.json",
|
|
3
3
|
"name": "blank",
|
|
4
4
|
"namespace": "blank",
|
|
5
|
-
"specVersion": "^
|
|
5
|
+
"specVersion": "^17.0.0",
|
|
6
6
|
"displayName": "Blank Starter",
|
|
7
7
|
"description": "Minimal ObjectStack environment with a single object — a clean slate for building.",
|
|
8
8
|
"category": "starter",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-objectstack",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.1.0",
|
|
4
4
|
"description": "Create a new ObjectStack project — npx create-objectstack",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-objectstack": "./bin/create-objectstack.js"
|
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"chalk": "^6.0.0",
|
|
19
|
-
"commander": "^15.0.0"
|
|
20
|
-
"tar": "^7.5.22"
|
|
19
|
+
"commander": "^15.0.0"
|
|
21
20
|
},
|
|
22
21
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^26.
|
|
22
|
+
"@types/node": "^26.2.0",
|
|
24
23
|
"tsup": "^8.5.1",
|
|
25
24
|
"typescript": "^6.0.3",
|
|
26
25
|
"vitest": "^4.1.10"
|