create-objectstack 17.1.0 → 17.2.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 +356 -0
- package/README.md +1 -1
- package/dist/chunk-ZIUW7UEA.js +112 -0
- package/dist/created-summary.d.ts +48 -0
- package/dist/created-summary.js +16 -0
- package/dist/index.js +99 -30
- package/dist/templates/AGENTS.md +1 -1
- package/dist/templates/blank/Dockerfile +2 -2
- package/dist/templates/blank/README.md +32 -8
- package/dist/templates/blank/docker-compose.yml +1 -1
- package/dist/templates/blank/objectstack.config.ts +19 -15
- package/dist/templates/blank/package.json +3 -0
- package/dist/templates/blank/pnpm-workspace.yaml +61 -2
- package/dist/templates/blank/src/objects/note.object.ts +6 -2
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,361 @@
|
|
|
1
1
|
# create-objectstack
|
|
2
2
|
|
|
3
|
+
## 17.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5a616d5: `create-objectstack` now closes with a "Created files" summary derived from a
|
|
8
|
+
walk of the finished project directory, so it names everything the run wrote —
|
|
9
|
+
including the files written after the template copy (#10323).
|
|
10
|
+
|
|
11
|
+
The old summary was the template copy's own list, printed before
|
|
12
|
+
`<pm> install` and before `npx skills add`. Measured against published
|
|
13
|
+
`create-objectstack@17.1.0` (`create-objectstack demo-app`, then a full walk of
|
|
14
|
+
the result): 12 entries printed, 18,045 paths on disk, **18,033 of them
|
|
15
|
+
unreachable from the summary** — `AGENTS.md`, `.github/copilot-instructions.md`,
|
|
16
|
+
`pnpm-lock.yaml`, `skills-lock.json`, `node_modules/`, and two ~968 KB trees of
|
|
17
|
+
agent instructions at `.agents/skills/` and `agent/skills/`.
|
|
18
|
+
|
|
19
|
+
That mattered because the same run ends with the `skills` CLI printing *"Review
|
|
20
|
+
skills before use; they run with full agent permissions."* Advice to review
|
|
21
|
+
files the run never named, at paths it never showed, is advice a newcomer
|
|
22
|
+
cannot act on — the wrong failure direction for a security-flavoured warning.
|
|
23
|
+
|
|
24
|
+
The list could not have been correct where it stood: two of the three write
|
|
25
|
+
phases belong to other processes, and the `skills` installer's destination set
|
|
26
|
+
moves with **its** releases, not ours. Reading the directory afterwards makes
|
|
27
|
+
the summary self-correcting instead. Large directories collapse to one line
|
|
28
|
+
carrying their path, entry count and size, so the bulk stays reviewable without
|
|
29
|
+
18,000 lines of output, and the paths the skills installer created are marked
|
|
30
|
+
`⚠ skills` with the permissions warning tied to them.
|
|
31
|
+
|
|
32
|
+
Same run, after the change: 20 entries printed, **0 written paths unreachable**.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- cec9d23: Fix `create-objectstack`'s startup banner hardcoding `◆ Create ObjectStack v6.x`
|
|
37
|
+
regardless of the package's real, released version — eleven majors stale, on
|
|
38
|
+
the first line of output a newcomer ever sees (#10325). The banner now calls
|
|
39
|
+
`readCliVersion()`, the same reader `.version()` already used, instead of a
|
|
40
|
+
literal string.
|
|
41
|
+
|
|
42
|
+
Dropping the real version in without recomputing the box's padding would have
|
|
43
|
+
reintroduced the same defect one line later — the border is a fixed run of
|
|
44
|
+
`═` computed for the 4-character `v6.x`, and a longer real version (`v17.1.0`
|
|
45
|
+
is 7 characters) would push the right border out of alignment (the sibling
|
|
46
|
+
bug fixed in #10322, one function away in the same file). The box now derives
|
|
47
|
+
its width from the version string's plain length and widens the frame — never
|
|
48
|
+
truncates — for a version long enough to need more room; ordinary versions
|
|
49
|
+
still render at the historical box size.
|
|
50
|
+
|
|
51
|
+
No behaviour change beyond the printed banner.
|
|
52
|
+
- 3a3f209: Tell a newcomer that the `blank` starter ships no app, so an empty Console
|
|
53
|
+
reads as the intended starting point rather than a broken install (#10317).
|
|
54
|
+
|
|
55
|
+
Measured on a real scaffold-and-boot (`create-objectstack my-app -t blank`,
|
|
56
|
+
published 17.1.0 packages, `objectstack dev --ui`): `GET /api/v1/meta/app`
|
|
57
|
+
returns the two platform apps (Setup, Account) and nothing of the project's
|
|
58
|
+
own, while `GET /api/v1/data/my_app_note` serves the scaffolded object the
|
|
59
|
+
whole time. The template ships `src/objects/` only — deliberately, as every
|
|
60
|
+
scaffolder template in this repo does — but nothing the newcomer could reach
|
|
61
|
+
said so, and `pnpm dev` advertises the Console URL on every boot.
|
|
62
|
+
|
|
63
|
+
Documentation only: a new "The Console" section in the generated `README.md`
|
|
64
|
+
naming the Console path, the consequence, and `src/apps/*.app.ts` as the
|
|
65
|
+
remedy. No change to what the scaffolder writes into `src/`.
|
|
66
|
+
- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in
|
|
67
|
+
the project `create-objectstack` scaffolds — at the canonical docs origin
|
|
68
|
+
`https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling.
|
|
69
|
+
|
|
70
|
+
Both spellings reach the same pages (the alias redirects to the apex,
|
|
71
|
+
path-preserving), so no link was broken. The reason it needs a release rather
|
|
72
|
+
than an in-repo fix alone: a README ships inside the npm tarball, so the
|
|
73
|
+
version already on npm keeps showing the old host to every reader of the
|
|
74
|
+
package page until a new one is published.
|
|
75
|
+
- 675ab57: **First-run polish:** a brand-new scaffold's very first `pnpm install` no longer reports two unmet peer dependencies (#10326).
|
|
76
|
+
|
|
77
|
+
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:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
✕ unmet peer better-call
|
|
81
|
+
Installed: 1.4.0
|
|
82
|
+
Wanted:
|
|
83
|
+
1.3.7:
|
|
84
|
+
@better-auth/scim@1.7.0-rc.1
|
|
85
|
+
|
|
86
|
+
✕ unmet peer better-sqlite3
|
|
87
|
+
Installed: 13.0.3
|
|
88
|
+
Wanted:
|
|
89
|
+
^12.0.0:
|
|
90
|
+
better-auth@1.7.1
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
**`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.
|
|
96
|
+
|
|
97
|
+
**`@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.
|
|
98
|
+
|
|
99
|
+
**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.
|
|
100
|
+
- e85182d: Converge the blank scaffold template's `README.md` docs links on the ruled
|
|
101
|
+
canonical origin, `https://objectstack.ai` (maintainer ruling, 2026-08-21:
|
|
102
|
+
「这个仓的文档站规范 URL 是 https://objectstack.ai」; enforced by
|
|
103
|
+
`CANONICAL_DOCS_ORIGIN` in `scripts/check-published-readme-links.mjs`). The
|
|
104
|
+
template previously linked the accepted-but-unratified `docs.objectstack.ai`
|
|
105
|
+
alias in three places, which disagreed with the root `README.md`'s already-
|
|
106
|
+
canonical spelling — so a single `npm create objectstack@latest` run handed
|
|
107
|
+
the user two different hostnames for the same docs site.
|
|
108
|
+
- aea1e64: Fix the declared bin (`bin/create-objectstack.js`) being tracked non-executable
|
|
109
|
+
in git. It carries a `#!/usr/bin/env node` shebang and is pnpm's link target
|
|
110
|
+
for the `create-objectstack` command, but was committed `100644` instead of
|
|
111
|
+
`100755` — matching the sibling declared bin `packages/cli/bin/run.js`, which
|
|
112
|
+
was already tracked executable.
|
|
113
|
+
|
|
114
|
+
Patch bump: this is a packaging-mode correction with no content, API or
|
|
115
|
+
behavior change (the blob hash is identical) — it only fixes how the file is
|
|
116
|
+
tracked in git and therefore how it is packed for npm.
|
|
117
|
+
- 818e027: Fix `objectstack init`'s closing "Created files" summary omitting `pnpm-lock.yaml` / `package-lock.json` and `node_modules/` (#10557).
|
|
118
|
+
|
|
119
|
+
The summary used to be printed from a list accumulated while the template
|
|
120
|
+
files were written — before `<pm> install` ran — so it could never name what
|
|
121
|
+
the package manager wrote. `init` now prints it after the install attempt
|
|
122
|
+
(succeeded or failed) from a walk of the finished project directory, reusing
|
|
123
|
+
`create-objectstack`'s `created-summary.ts` (now published as the
|
|
124
|
+
`create-objectstack/created-summary` subpath) instead of a second copy of the
|
|
125
|
+
same renderer.
|
|
126
|
+
- afe1c4e: fix(cli): declare the four `@better-auth/utils` peer skews a freshly scaffolded project reports (#10931)
|
|
127
|
+
|
|
128
|
+
Both scaffold paths emit a `peerDependencyRules.allowedVersions` block whose
|
|
129
|
+
stated purpose is that a brand-new project's first `pnpm install` does not open
|
|
130
|
+
with a peer-skew report. It declared two skews and left four showing:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
├─┬ @better-auth/core 1.7.1
|
|
134
|
+
│ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
|
|
135
|
+
├─┬ @better-auth/scim 1.7.0-rc.1
|
|
136
|
+
│ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
|
|
137
|
+
├─┬ @better-auth/oauth-provider 1.7.1
|
|
138
|
+
│ └── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
|
|
139
|
+
└─┬ @better-auth/sso 1.7.1
|
|
140
|
+
└── ✕ unmet peer @better-auth/utils@0.4.2: found 0.5.0
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`@better-auth/core`, `/oauth-provider`, `/scim` and `/sso` each peer an **exact**
|
|
144
|
+
`@better-auth/utils@0.4.2`. The 0.5.0 they are handed comes from
|
|
145
|
+
`better-call@1.4.0` — better-auth's own HTTP layer — which *depends* on
|
|
146
|
+
`^0.5.0`; `@objectstack/plugin-auth` names the four as direct dependencies
|
|
147
|
+
without naming utils, so pnpm satisfies their peer from better-call's copy
|
|
148
|
+
instead of better-auth's own exact 0.4.2 dependency.
|
|
149
|
+
|
|
150
|
+
**Measured compatible before widening, not assumed.** Those four import three
|
|
151
|
+
symbols in total: `base64`/`base64Url` (`@better-auth/utils/base64`),
|
|
152
|
+
`createHash` (`/hash`) and, in core only, `createRandomStringGenerator`
|
|
153
|
+
(`/random`). 0.5.0 declares all three with identical signatures; `/random` is
|
|
154
|
+
unchanged apart from formatting, `/base64` swaps `new Uint8Array(data)` for a
|
|
155
|
+
helper that *is* `new Uint8Array(data)` on non-strings, and `/hash` only widens
|
|
156
|
+
its input coercion for views not backed by a plain `ArrayBuffer`. Run against
|
|
157
|
+
the input shapes those call sites actually pass, the two versions agree on every
|
|
158
|
+
value; run end to end — better-auth with the `sso`, `oauth-provider` and `scim`
|
|
159
|
+
plugins — a tree where the four resolve 0.5.0 and one where they resolve 0.4.2
|
|
160
|
+
produce the same transcript: sign-up, sign-in, session, both OAuth metadata
|
|
161
|
+
documents, the RFC 7636 PKCE challenge, and the SCIM and SSO endpoint outcomes.
|
|
162
|
+
|
|
163
|
+
A resolution change was measured too, and rejected: pinning utils back to 0.4.2
|
|
164
|
+
clears the four lines only by dragging `better-call@1.4.0` off its own declared
|
|
165
|
+
`^0.5.0` — manufacturing one real range violation to silence four benign ones.
|
|
166
|
+
|
|
167
|
+
Four scoped entries, one per declaring package, matching the block's convention
|
|
168
|
+
that each rule widens exactly one declaration. `allowedVersions` suppresses the
|
|
169
|
+
report only: the lockfile a scaffold resolves is byte-identical with and without
|
|
170
|
+
the block. The version is spelled `0.5.0` exactly rather than `0.5`, so a future
|
|
171
|
+
`0.6.0` reports again instead of inheriting this finding.
|
|
172
|
+
|
|
173
|
+
Both scaffold paths — `objectstack init` (rendered by the CLI) and
|
|
174
|
+
`npx create-objectstack` (a copied template file) — are changed together, and
|
|
175
|
+
`packages/cli/test/scaffold-workspace-consistency.test.ts` gains a limb that
|
|
176
|
+
compares the peer maps the two produce, so they cannot drift apart again.
|
|
177
|
+
- 21756b3: fix(create-objectstack): converge scaffold docs on the canonical host and drop the last two dead monorepo references (#10990, #11022)
|
|
178
|
+
|
|
179
|
+
A freshly scaffolded project shipped a handful of text lines a reader with
|
|
180
|
+
only their own project — no monorepo, no `docs/adr/`, no issue tracker —
|
|
181
|
+
could not follow:
|
|
182
|
+
|
|
183
|
+
- `templates/AGENTS.md` linked `https://objectstack.com/docs`, a domain that
|
|
184
|
+
is not this project's docs site at all (not even a redirecting alias).
|
|
185
|
+
- `templates/blank/Dockerfile` and `templates/blank/docker-compose.yml` both
|
|
186
|
+
linked `https://docs.objectstack.ai/...`, an accepted-but-unratified alias.
|
|
187
|
+
All three now point at the ruled canonical origin, `https://objectstack.ai`
|
|
188
|
+
(maintainer ruling, 2026-08-21).
|
|
189
|
+
- `templates/blank/README.md` cited `ADR-0097` and named "the ObjectStack
|
|
190
|
+
framework repo" as the home of `skills/` — both rewritten self-contained,
|
|
191
|
+
keeping the fact each was carrying: the connector-materialization line now
|
|
192
|
+
links the public [Automation → Connectors](https://objectstack.ai/docs/automation/connectors)
|
|
193
|
+
page, and the skills line now names the followable
|
|
194
|
+
`npx skills add objectstack-ai/objectstack/skills` install the scaffolder's
|
|
195
|
+
own closing output already uses.
|
|
196
|
+
|
|
197
|
+
`packages/create-objectstack/src/starter-comments-self-contained.test.ts`
|
|
198
|
+
(#10324) gains two pin obligations these two fixes call for: a host-convergence
|
|
199
|
+
assertion driven by the same `shippedFiles()` walker that already enumerates
|
|
200
|
+
everything a scaffold ships (no other repo gate's population reaches these
|
|
201
|
+
template files — `check:published-readme-links` reads publishable packages'
|
|
202
|
+
published markdown only), and a fifth `MONOREPO_ONLY` pattern that catches a
|
|
203
|
+
prose-shaped reference to this repo ("the ObjectStack framework repo") the
|
|
204
|
+
first four, syntax-shaped patterns could not. The self-retiring `EXCLUDED`
|
|
205
|
+
entry for `blank/README.md` is removed now that the file cites nothing
|
|
206
|
+
monorepo-only.
|
|
207
|
+
- 568de19: Scaffolded projects declare an explicit empty `packages: []` in their
|
|
208
|
+
`pnpm-workspace.yaml` (#10933). Both scaffold paths render it —
|
|
209
|
+
`renderPnpmWorkspaceYaml` in `objectstack init`, and the bundled `blank`
|
|
210
|
+
template `npx create-objectstack` copies.
|
|
211
|
+
|
|
212
|
+
The file was deliberately keyless so it would act purely as a settings file.
|
|
213
|
+
That intent is now written down rather than inferred from a missing key, and
|
|
214
|
+
writing it down is what fixes a first-command failure: pnpm 9.x and 10.0–10.4
|
|
215
|
+
parse `pnpm-workspace.yaml` **before** they read `engines`, so they refused a
|
|
216
|
+
brand-new project outright with
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
ERROR packages field missing or empty
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
naming a file the user never wrote and giving no hint that the cause is their
|
|
223
|
+
pnpm version — and no `engines.pnpm` floor could reach them, because they never
|
|
224
|
+
got as far as the engines check. Measured, one clean install per pnpm version,
|
|
225
|
+
each with its own store:
|
|
226
|
+
|
|
227
|
+
| pnpm | before | after |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| 9.15.9, 10.0.0, 10.4.0 | `ERROR packages field missing or empty` | `ERR_PNPM_UNSUPPORTED_ENGINE`, naming `>=10.15` |
|
|
230
|
+
| 10.5.0–10.14.0 | `ERR_PNPM_UNSUPPORTED_ENGINE` | unchanged |
|
|
231
|
+
| 10.15.0, 10.34.5, 11.22.0 | installs | installs, byte-identical `pnpm-lock.yaml` |
|
|
232
|
+
|
|
233
|
+
So every unsupported pnpm now reports the same actionable cause, and supported
|
|
234
|
+
pnpm is unaffected: the empty key was measured equivalent to omission on
|
|
235
|
+
10.15.0, 10.34.5 and 11.22.0 — identical lockfile bytes, identical
|
|
236
|
+
`node_modules/.modules.yaml` once the run-local `prunedAt`/`storeDir` fields are
|
|
237
|
+
dropped, identical `pnpm ls -r --depth -1`, and an identical second-install
|
|
238
|
+
"Already up to date".
|
|
239
|
+
|
|
240
|
+
The declaration is an **empty** list on purpose. `packages: ['.']` satisfies the
|
|
241
|
+
same parsers but declares the project root a workspace *member* — a monorepo
|
|
242
|
+
root — which a single-package scaffold is not, and which reads to the next
|
|
243
|
+
author (human or AI) as an invitation to add member packages to an app.
|
|
244
|
+
|
|
245
|
+
`engines.pnpm` is unchanged at `>=10.15`.
|
|
246
|
+
- 8d21f7a: Fix `create-objectstack`'s closing "Next steps" and install-failure remedy
|
|
247
|
+
hardcoding `npm` regardless of which package manager the run actually used
|
|
248
|
+
(#10322). `detectPackageManager()` already prefers `pnpm` and falls back to
|
|
249
|
+
`npm` only when `pnpm` is unreachable — confirmed still true at HEAD, and
|
|
250
|
+
confirmed empirically: a real run with `pnpm` on `PATH` installs with `pnpm`
|
|
251
|
+
(`pnpm-lock.yaml`, "Done in … using pnpm vX") and then told the newcomer to
|
|
252
|
+
run `npm run dev` / `npm run validate` next, a package manager the run never
|
|
253
|
+
touched. The detected package manager is now read once, up front, and reused
|
|
254
|
+
consistently for the install command, the install-failure remedy, and every
|
|
255
|
+
line of "Next steps" — so the printed guidance always names the tool the run
|
|
256
|
+
actually used, in both the `pnpm` and the `npm`-fallback case.
|
|
257
|
+
|
|
258
|
+
Also names `validate` — the step the generated `AGENTS.md` calls
|
|
259
|
+
unskippable — in the "Getting started" section of the generated `blank`
|
|
260
|
+
template's README, not only in its later "Verify your changes" section, so a
|
|
261
|
+
newcomer reading top-to-bottom sees it at first touch.
|
|
262
|
+
|
|
263
|
+
No install behaviour changes: the scaffolder still installs by default and
|
|
264
|
+
still supports `--skip-install`; this is a messaging-only fix.
|
|
265
|
+
- 9d101d2: Declare a pnpm floor (`engines.pnpm: ">=10.15"`) in the `package.json` both
|
|
266
|
+
scaffolders write, so an unsupported pnpm reports its own version instead of an
|
|
267
|
+
error about a file the user never wrote.
|
|
268
|
+
|
|
269
|
+
Both scaffold paths emit a settings-only `pnpm-workspace.yaml` with no
|
|
270
|
+
`packages:` key. Early pnpm 10 refuses that file outright — `pnpm install` exits
|
|
271
|
+
1 with `ERROR packages field missing or empty` before resolving a single
|
|
272
|
+
dependency, so a brand-new project could not be installed at all. Measured on
|
|
273
|
+
the rendered shape, one clean install per pnpm version, each with its own store:
|
|
274
|
+
|
|
275
|
+
| pnpm | before | after |
|
|
276
|
+
| --- | --- | --- |
|
|
277
|
+
| 10.0.0 – 10.4.0 | `packages field missing or empty` | unchanged — see below |
|
|
278
|
+
| 10.5.0 – 10.14.0 | `packages field missing or empty` | `ERR_PNPM_UNSUPPORTED_ENGINE`, naming the expected range |
|
|
279
|
+
| >= 10.15.0 | installs | installs |
|
|
280
|
+
|
|
281
|
+
The floor is a diagnosis, not a repair: pnpm 10.0.0–10.4.0 parse
|
|
282
|
+
`pnpm-workspace.yaml` *before* they read `engines`, so they still print the raw
|
|
283
|
+
workspace error. Closing that remaining sliver requires deciding what a
|
|
284
|
+
single-package scaffold should declare under `packages:`, which is tracked
|
|
285
|
+
separately and deliberately not decided here.
|
|
286
|
+
|
|
287
|
+
`engines.pnpm` rather than a `packageManager` stamp: npm, yarn and bun ignore
|
|
288
|
+
`engines.pnpm` entirely, so the scaffold keeps working for all four package
|
|
289
|
+
managers `objectstack init` hands off to. A `packageManager: "pnpm@x.y.z"` stamp
|
|
290
|
+
would declare the project pnpm-only (corepack-driven yarn refuses to run in such
|
|
291
|
+
a project) and pin one exact version that goes stale on every pnpm release — and
|
|
292
|
+
it buys nothing on 10.0–10.4, which reach the workspace error before reading
|
|
293
|
+
that field either.
|
|
294
|
+
|
|
295
|
+
No existing project is affected; this only changes what a newly scaffolded
|
|
296
|
+
`package.json` contains.
|
|
297
|
+
- 6d441e4: Correct the pnpm boundary the blank template states for `allowBuilds`, and gate
|
|
298
|
+
the two scaffold paths against each other (#10498, #10499).
|
|
299
|
+
|
|
300
|
+
`packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml` is copied
|
|
301
|
+
verbatim into every scaffolded project, so its header comment is prose that
|
|
302
|
+
ships **inside the user's own repository**. It said `allowBuilds` needs
|
|
303
|
+
pnpm >= 10.31 and that `onlyBuiltDependencies` covers pnpm 10.0–10.30. Measured
|
|
304
|
+
on a probe depending on `esbuild@0.28.2`, with a workspace file carrying only
|
|
305
|
+
`allowBuilds`, one clean install per pnpm version and each with its own
|
|
306
|
+
`--store-dir` (isolation matters — pnpm's side-effects cache will otherwise hand
|
|
307
|
+
a later run a build an earlier run performed, and it reads as "the key worked"):
|
|
308
|
+
|
|
309
|
+
| pnpm | `allowBuilds` alone |
|
|
310
|
+
|:--|:--|
|
|
311
|
+
| 10.15.0 – 10.25.0 | ignored — build not run |
|
|
312
|
+
| **10.26.0** | **honoured — build ran** |
|
|
313
|
+
| 10.28.0 – 10.33.0 | honoured — build ran |
|
|
314
|
+
|
|
315
|
+
So the floor is 10.26.0 and the older-key band is 10.0–10.25. A user on pnpm
|
|
316
|
+
10.28 was being told by the file in front of them that their pnpm cannot read
|
|
317
|
+
the key it is in fact reading. Both load-bearing claims in that comment were
|
|
318
|
+
correct and are unchanged: both keys are needed, and pnpm 11 reads only
|
|
319
|
+
`allowBuilds`. No setting, no assertion and no install behaviour changes — the
|
|
320
|
+
rendered `onlyBuiltDependencies` / `allowBuilds` values are byte-identical.
|
|
321
|
+
|
|
322
|
+
The reason it was wrong for so long is the second half of this change.
|
|
323
|
+
`objectstack init` renders the same file from `renderPnpmWorkspaceYaml()` in
|
|
324
|
+
`packages/cli`, it was corrected to the measured numbers separately, and each
|
|
325
|
+
package's ratchets are package-local — so neither could ever fail for the other
|
|
326
|
+
file's regression, and the two scaffold paths shipped contradictory prose about
|
|
327
|
+
the same rule with every gate green. `packages/cli/test/scaffold-workspace-consistency.test.ts`
|
|
328
|
+
now compares the two **rendered outputs**: the packages each key actually grants
|
|
329
|
+
a build to, and the pnpm versions each file actually names for each key. It was
|
|
330
|
+
confirmed failing against the live divergence before this correction landed.
|
|
331
|
+
|
|
332
|
+
Bumped `patch` rather than left out: the corrected text is user-visible — it is
|
|
333
|
+
delivered into every new project — while nothing executable moves.
|
|
334
|
+
- ecd06f6: Rewrite the scaffolded project's starter comments so a newcomer can actually
|
|
335
|
+
follow them (#10324). `objectstack.config.ts` and `src/objects/note.object.ts`
|
|
336
|
+
are the first two files opened after scaffolding, and between them they cited
|
|
337
|
+
four ADR identifiers, one bare issue number and the path of a release-time
|
|
338
|
+
script in this monorepo — none of which ship in, or are linked from, a
|
|
339
|
+
scaffolded project. `// per ADR-0097` read as a reference the reader was
|
|
340
|
+
failing to follow rather than as the context it was meant to be.
|
|
341
|
+
|
|
342
|
+
The explanations are kept and made self-contained; only the dead ends are
|
|
343
|
+
gone. Each now states the fact the identifier stood for — the protocol range
|
|
344
|
+
is checked before anything loads and was stamped to match the installed
|
|
345
|
+
version rather than hand-tuned; `automation` must stay whenever `plugins:`
|
|
346
|
+
lists a connector or the executors have nowhere to register; a declarative
|
|
347
|
+
`mcp` stdio transport is denied by default; the org-wide default is required
|
|
348
|
+
so the baseline is an authored decision — and points at the public docs page
|
|
349
|
+
that covers it in full. The blank `Dockerfile` likewise stops pointing at a
|
|
350
|
+
file in this repo and points at the self-hosting guide it already links.
|
|
351
|
+
|
|
352
|
+
A pin (`starter-comments-self-contained.test.ts`) keeps it that way from both
|
|
353
|
+
sides: no shipped template file may cite an ADR identifier, a bare issue
|
|
354
|
+
number or a repo script path, and the facts those references carried must
|
|
355
|
+
still be stated — so the comments cannot be "fixed" by deleting them. It also
|
|
356
|
+
resolves every canonical-origin docs URL in the shipped tree against
|
|
357
|
+
`content/docs`, because a link that 404s is the same defect one level out.
|
|
358
|
+
|
|
3
359
|
## 17.1.0
|
|
4
360
|
|
|
5
361
|
### 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://
|
|
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
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describeEntry,
|
|
3
|
+
summarizeTree
|
|
4
|
+
} from "./chunk-ZIUW7UEA.js";
|
|
5
|
+
|
|
1
6
|
// src/index.ts
|
|
2
7
|
import { Command } from "commander";
|
|
3
|
-
import
|
|
8
|
+
import chalk2 from "chalk";
|
|
4
9
|
import fs4 from "fs";
|
|
5
10
|
import path4 from "path";
|
|
6
11
|
import { execSync } from "child_process";
|
|
@@ -192,6 +197,26 @@ function pinRuntimeImage(targetDir, version) {
|
|
|
192
197
|
return { pinned: true, tag: version };
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
// src/banner.ts
|
|
201
|
+
import chalk from "chalk";
|
|
202
|
+
var PREFIX = " \u25C6 Create ObjectStack ";
|
|
203
|
+
var MIN_INNER_WIDTH = 35;
|
|
204
|
+
var MIN_TRAILING_PAD = 3;
|
|
205
|
+
function renderVersionBanner(version) {
|
|
206
|
+
const versionLabel = `v${version}`;
|
|
207
|
+
const innerWidth = Math.max(
|
|
208
|
+
MIN_INNER_WIDTH,
|
|
209
|
+
PREFIX.length + versionLabel.length + MIN_TRAILING_PAD
|
|
210
|
+
);
|
|
211
|
+
const trailingPad = innerWidth - PREFIX.length - versionLabel.length;
|
|
212
|
+
const border = "\u2550".repeat(innerWidth);
|
|
213
|
+
return [
|
|
214
|
+
chalk.bold.cyan(` \u2554${border}\u2557`),
|
|
215
|
+
chalk.bold.cyan(" \u2551") + chalk.bold(PREFIX) + chalk.dim(versionLabel) + chalk.bold.cyan(`${" ".repeat(trailingPad)}\u2551`),
|
|
216
|
+
chalk.bold.cyan(` \u255A${border}\u255D`)
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
195
220
|
// src/index.ts
|
|
196
221
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
197
222
|
var __dirname2 = path4.dirname(__filename2);
|
|
@@ -220,24 +245,24 @@ function readCliVersion() {
|
|
|
220
245
|
}
|
|
221
246
|
}
|
|
222
247
|
function printHeader(title) {
|
|
223
|
-
console.log(
|
|
248
|
+
console.log(chalk2.bold(`
|
|
224
249
|
\u25C6 ${title}`));
|
|
225
|
-
console.log(
|
|
250
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
226
251
|
}
|
|
227
252
|
function printKV(key, value) {
|
|
228
|
-
console.log(` ${
|
|
253
|
+
console.log(` ${chalk2.dim(key + ":")} ${chalk2.white(value)}`);
|
|
229
254
|
}
|
|
230
255
|
function printSuccess(msg) {
|
|
231
|
-
console.log(
|
|
256
|
+
console.log(chalk2.green(` \u2713 ${msg}`));
|
|
232
257
|
}
|
|
233
258
|
function printError(msg) {
|
|
234
|
-
console.log(
|
|
259
|
+
console.log(chalk2.red(` \u2717 ${msg}`));
|
|
235
260
|
}
|
|
236
261
|
function printStep(msg) {
|
|
237
|
-
console.log(
|
|
262
|
+
console.log(chalk2.yellow(` \u2192 ${msg}`));
|
|
238
263
|
}
|
|
239
264
|
function printWarning(msg) {
|
|
240
|
-
console.log(
|
|
265
|
+
console.log(chalk2.yellow(` \u26A0 ${msg}`));
|
|
241
266
|
}
|
|
242
267
|
function detectPackageManager() {
|
|
243
268
|
try {
|
|
@@ -328,6 +353,49 @@ function writeAgentGuides(targetDir, title, projectName) {
|
|
|
328
353
|
fs4.mkdirSync(path4.dirname(copilotPath), { recursive: true });
|
|
329
354
|
writeIfAbsent(copilotPath, rendered);
|
|
330
355
|
}
|
|
356
|
+
function topLevelNames(dir) {
|
|
357
|
+
try {
|
|
358
|
+
return new Set(fs4.readdirSync(dir));
|
|
359
|
+
} catch {
|
|
360
|
+
return /* @__PURE__ */ new Set();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function printCreatedSummary(targetDir, opts) {
|
|
364
|
+
const entries = summarizeTree(targetDir);
|
|
365
|
+
if (entries.length === 0) return;
|
|
366
|
+
console.log(
|
|
367
|
+
chalk2.bold(opts.wasEmpty ? " Created files:" : " Project contents:")
|
|
368
|
+
);
|
|
369
|
+
if (!opts.wasEmpty) {
|
|
370
|
+
console.log(
|
|
371
|
+
chalk2.dim(" (the directory already had contents; this lists all of it)")
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const isSkillPath = (p) => opts.skillPaths.has(p.split("/")[0]);
|
|
375
|
+
const width = Math.min(
|
|
376
|
+
44,
|
|
377
|
+
Math.max(...entries.map((e) => e.path.length)) + 2
|
|
378
|
+
);
|
|
379
|
+
let flagged = false;
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
const note = describeEntry(entry);
|
|
382
|
+
const flag = isSkillPath(entry.path);
|
|
383
|
+
if (flag) flagged = true;
|
|
384
|
+
const pad = note || flag ? entry.path.padEnd(width) : entry.path;
|
|
385
|
+
const line = ` + ${pad}${note ? chalk2.dim(note) : ""}`;
|
|
386
|
+
console.log(chalk2.green(line) + (flag ? chalk2.yellow(" \u26A0 skills") : ""));
|
|
387
|
+
}
|
|
388
|
+
if (flagged) {
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log(
|
|
391
|
+
chalk2.yellow(" \u26A0 Skill files run with your coding agent's full permissions.")
|
|
392
|
+
);
|
|
393
|
+
console.log(
|
|
394
|
+
chalk2.dim(" Review the paths marked \u26A0 above before letting an agent use them.")
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
console.log("");
|
|
398
|
+
}
|
|
331
399
|
function writeIfAbsent(filePath, contents) {
|
|
332
400
|
try {
|
|
333
401
|
fs4.writeFileSync(filePath, contents, { flag: "wx" });
|
|
@@ -341,23 +409,21 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
341
409
|
"blank"
|
|
342
410
|
).option("--skip-install", "Skip dependency installation").option("--skip-skills", "Skip installing ObjectStack AI skills").action((name, options) => {
|
|
343
411
|
console.log("");
|
|
344
|
-
console.log(
|
|
345
|
-
console.log(chalk.bold.cyan(" \u2551") + chalk.bold(" \u25C6 Create ObjectStack ") + chalk.dim("v6.x") + chalk.bold.cyan(" \u2551"));
|
|
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"));
|
|
412
|
+
for (const line of renderVersionBanner(readCliVersion())) console.log(line);
|
|
347
413
|
printHeader("New Environment");
|
|
348
414
|
const lookup = lookupTemplate(options.template);
|
|
349
415
|
if (lookup.kind !== "found") {
|
|
350
416
|
if (lookup.kind === "retired") {
|
|
351
417
|
printError(`Template "${lookup.name}" has been retired and is no longer available.`);
|
|
352
418
|
console.log(
|
|
353
|
-
|
|
419
|
+
chalk2.dim(
|
|
354
420
|
" It was delisted from the ObjectStack template marketplace and is no longer maintained."
|
|
355
421
|
)
|
|
356
422
|
);
|
|
357
423
|
} else {
|
|
358
424
|
printError(`Unknown template: ${lookup.name}`);
|
|
359
425
|
}
|
|
360
|
-
console.log(
|
|
426
|
+
console.log(chalk2.dim(` Available: ${templateNames().join(", ")}`));
|
|
361
427
|
process.exit(1);
|
|
362
428
|
}
|
|
363
429
|
const template = lookup.template;
|
|
@@ -366,6 +432,7 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
366
432
|
const namespace = sanitizeNamespace(projectName);
|
|
367
433
|
const targetDir = name ? path4.resolve(cwd, name) : cwd;
|
|
368
434
|
const isCurrentDir = targetDir === cwd;
|
|
435
|
+
const pm = detectPackageManager();
|
|
369
436
|
printKV("Environment", projectName);
|
|
370
437
|
printKV("Namespace", namespace);
|
|
371
438
|
printKV("Template", `${options.template} \u2014 ${template.description}`);
|
|
@@ -378,28 +445,22 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
378
445
|
process.exit(1);
|
|
379
446
|
}
|
|
380
447
|
}
|
|
448
|
+
const targetWasEmpty = topLevelNames(targetDir).size === 0;
|
|
381
449
|
try {
|
|
382
450
|
fs4.mkdirSync(targetDir, { recursive: true });
|
|
383
451
|
const createdFiles = loadBundled(template.source.dir, targetDir);
|
|
384
452
|
rewriteProjectIdentity(targetDir, projectName, namespace);
|
|
385
|
-
|
|
386
|
-
for (const f of createdFiles.slice(0, 20)) {
|
|
387
|
-
console.log(chalk.green(` + ${f}`));
|
|
388
|
-
}
|
|
389
|
-
if (createdFiles.length > 20) {
|
|
390
|
-
console.log(chalk.dim(` \u2026 and ${createdFiles.length - 20} more`));
|
|
391
|
-
}
|
|
453
|
+
printSuccess(`Template files written (${createdFiles.length})`);
|
|
392
454
|
console.log("");
|
|
393
455
|
if (!options.skipInstall) {
|
|
394
456
|
printStep("Installing dependencies...");
|
|
395
457
|
let installed = false;
|
|
396
458
|
try {
|
|
397
|
-
const pm = detectPackageManager();
|
|
398
459
|
execSync(`${pm} install`, { stdio: "inherit", cwd: targetDir });
|
|
399
460
|
installed = true;
|
|
400
461
|
console.log("");
|
|
401
462
|
} catch {
|
|
402
|
-
printWarning(
|
|
463
|
+
printWarning(`Dependency installation failed. Run \`${pm} install\` manually.`);
|
|
403
464
|
console.log("");
|
|
404
465
|
}
|
|
405
466
|
if (installed) {
|
|
@@ -417,6 +478,7 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
417
478
|
}
|
|
418
479
|
}
|
|
419
480
|
}
|
|
481
|
+
const beforeSkills = topLevelNames(targetDir);
|
|
420
482
|
if (!options.skipInstall && !options.skipSkills) {
|
|
421
483
|
printStep("Installing AI skills for your coding agent...");
|
|
422
484
|
try {
|
|
@@ -432,22 +494,29 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
432
494
|
console.log("");
|
|
433
495
|
}
|
|
434
496
|
}
|
|
497
|
+
const skillPaths = new Set(
|
|
498
|
+
[...topLevelNames(targetDir)].filter((p) => !beforeSkills.has(p))
|
|
499
|
+
);
|
|
500
|
+
printCreatedSummary(targetDir, { wasEmpty: targetWasEmpty, skillPaths });
|
|
435
501
|
printSuccess("Environment created!");
|
|
436
502
|
console.log("");
|
|
437
|
-
console.log(
|
|
503
|
+
console.log(chalk2.bold(" Next steps:"));
|
|
438
504
|
if (!isCurrentDir) {
|
|
439
|
-
console.log(
|
|
505
|
+
console.log(chalk2.dim(` cd ${name}`));
|
|
440
506
|
}
|
|
441
507
|
if (options.skipInstall) {
|
|
442
|
-
console.log(
|
|
508
|
+
console.log(chalk2.dim(` ${pm} install`));
|
|
443
509
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
510
|
+
const devLabel = `${pm} run dev`;
|
|
511
|
+
const validateLabel = `${pm} run validate`;
|
|
512
|
+
const labelWidth = Math.max(devLabel.length, validateLabel.length) + 3;
|
|
513
|
+
console.log(chalk2.dim(` ${devLabel.padEnd(labelWidth)}# Start development server`));
|
|
514
|
+
console.log(chalk2.dim(` ${validateLabel.padEnd(labelWidth)}# Verify metadata: schema + predicates + bindings`));
|
|
515
|
+
console.log(chalk2.dim(` ${" ".repeat(labelWidth)}# (run after every metadata edit \u2014 see AGENTS.md)`));
|
|
447
516
|
if (options.skipInstall || options.skipSkills) {
|
|
448
517
|
console.log("");
|
|
449
|
-
console.log(
|
|
450
|
-
console.log(
|
|
518
|
+
console.log(chalk2.bold(" AI Skills (recommended):"));
|
|
519
|
+
console.log(chalk2.dim(" npx skills add objectstack-ai/objectstack/skills"));
|
|
451
520
|
}
|
|
452
521
|
console.log("");
|
|
453
522
|
} catch (error) {
|
package/dist/templates/AGENTS.md
CHANGED
|
@@ -96,6 +96,6 @@ Skills are triggered automatically based on task context:
|
|
|
96
96
|
|
|
97
97
|
## Learn More
|
|
98
98
|
|
|
99
|
-
- [ObjectStack Documentation](https://objectstack.
|
|
99
|
+
- [ObjectStack Documentation](https://objectstack.ai/docs)
|
|
100
100
|
- [GitHub: objectstack-ai/objectstack](https://github.com/objectstack-ai/objectstack)
|
|
101
101
|
- [Skills CLI](https://skills.sh/) — Manage AI skills across agents
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# my-app
|
|
8
8
|
#
|
|
9
9
|
# Or run the full app + Postgres stack: see docker-compose.yml.
|
|
10
|
-
# Docs: https://
|
|
10
|
+
# Docs: https://objectstack.ai/docs/deployment/self-hosting
|
|
11
11
|
|
|
12
12
|
# ── Build stage: compile TypeScript metadata to the artifact ─────────
|
|
13
13
|
FROM node:22-slim AS build
|
|
@@ -20,7 +20,7 @@ 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
|
|
23
|
+
# — see the self-hosting guide linked above.
|
|
24
24
|
#
|
|
25
25
|
# Dependencies were not installed while scaffolding, so the tag below could
|
|
26
26
|
# not be resolved for you. `latest` floats to whatever release is newest,
|
|
@@ -9,6 +9,11 @@ pnpm install
|
|
|
9
9
|
pnpm dev
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
After editing any metadata (an object, view, flow, …), run `pnpm validate` —
|
|
13
|
+
see [Verify your changes](#verify-your-changes) below. It is the one command
|
|
14
|
+
this project's `AGENTS.md` calls unskippable: it catches mistakes that
|
|
15
|
+
otherwise fail silently at runtime.
|
|
16
|
+
|
|
12
17
|
The REST API is served at `http://localhost:3000/api/v1`. Data endpoints
|
|
13
18
|
require a session — the dev server seeds a login-ready admin
|
|
14
19
|
(`admin@objectos.ai` / `admin123`) on an empty database:
|
|
@@ -21,6 +26,23 @@ curl -c cookies.txt -X POST http://localhost:3000/api/v1/auth/sign-in/email \
|
|
|
21
26
|
curl -b cookies.txt "http://localhost:3000/api/v1/data/<your_object>"
|
|
22
27
|
```
|
|
23
28
|
|
|
29
|
+
## The Console — this starter ships no app
|
|
30
|
+
|
|
31
|
+
`pnpm dev` also serves the admin Console at `http://localhost:3000/_console/`,
|
|
32
|
+
and prints the link on boot. Open it and you will see the platform's own apps
|
|
33
|
+
(Setup, Account) and **not** the object in `src/objects/` — this starter ships
|
|
34
|
+
objects only, with no app and no views.
|
|
35
|
+
|
|
36
|
+
That is the intended starting point, not a broken install. The object is live
|
|
37
|
+
the whole time — the `curl` above returns it, and an MCP client can read and
|
|
38
|
+
write it. What it has no route into is the Console's navigation.
|
|
39
|
+
|
|
40
|
+
**An object appears in Console navigation only when an app lists it.** Add an
|
|
41
|
+
`*.app.ts` under `src/apps/` (plus the views it points at), and the Console
|
|
42
|
+
renders it after the next `pnpm dev` rebuild. The `objectstack-ui` skill covers
|
|
43
|
+
the shape; describing the app you want to your coding agent is the intended
|
|
44
|
+
path.
|
|
45
|
+
|
|
24
46
|
## Your app is an MCP server
|
|
25
47
|
|
|
26
48
|
Every ObjectStack app is itself a
|
|
@@ -36,7 +58,7 @@ claude mcp add --transport http my-app http://localhost:3000/api/v1/mcp
|
|
|
36
58
|
|
|
37
59
|
Set `OS_MCP_SERVER_ENABLED=false` to turn it off. This is the *serve* side — the
|
|
38
60
|
reverse of the `mcp` connector below (which lets your app *call* other MCP
|
|
39
|
-
servers). See [Connect an MCP Client](https://
|
|
61
|
+
servers). See [Connect an MCP Client](https://objectstack.ai/docs/ai/connect-mcp)
|
|
40
62
|
for OAuth, API keys, and which objects/actions become tools.
|
|
41
63
|
|
|
42
64
|
## Layout
|
|
@@ -57,9 +79,11 @@ can call an external system from a flow as pure metadata — no host code:
|
|
|
57
79
|
|
|
58
80
|
Add a `connectors:` entry that names one of these `provider`s and the
|
|
59
81
|
`automation` capability materializes it into a live, dispatchable connector at
|
|
60
|
-
boot
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
boot — see [Automation → Connectors](https://objectstack.ai/docs/automation/connectors)
|
|
83
|
+
for how that materialization works; a flow's `connector_action` node then
|
|
84
|
+
calls it. To add a brand connector (e.g. Slack), install its package and add
|
|
85
|
+
`new ConnectorSlackPlugin()` to `plugins:`; to drop a provider, remove its
|
|
86
|
+
plugin.
|
|
63
87
|
|
|
64
88
|
> **Security — declarative MCP over stdio.** An `mcp` connector whose transport
|
|
65
89
|
> spawns a local process (`stdio`) is denied by default, because the command
|
|
@@ -67,7 +91,7 @@ to `plugins:`; to drop a provider, remove its plugin.
|
|
|
67
91
|
> `new ConnectorMcpPlugin({ declarativeStdio: ['node'] })`; `http` transports
|
|
68
92
|
> need no opt-in.
|
|
69
93
|
|
|
70
|
-
See [Automation → Flows](https://
|
|
94
|
+
See [Automation → Flows](https://objectstack.ai/docs/automation/flows) for
|
|
71
95
|
the full connector and `connector_action` guide.
|
|
72
96
|
|
|
73
97
|
## Verify your changes
|
|
@@ -105,7 +129,7 @@ curl -fsS http://localhost:8080/api/v1/health
|
|
|
105
129
|
```
|
|
106
130
|
|
|
107
131
|
Bare Node, Kubernetes, reverse-proxy wiring, and the required secrets are
|
|
108
|
-
covered in [Self-Hosted Deployment](https://
|
|
132
|
+
covered in [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-hosting).
|
|
109
133
|
|
|
110
134
|
## Next steps
|
|
111
135
|
|
|
@@ -114,5 +138,5 @@ covered in [Self-Hosted Deployment](https://docs.objectstack.ai/docs/deployment/
|
|
|
114
138
|
- Add a flow or automation: see `objectstack-automation`.
|
|
115
139
|
- Add an AI agent: see `objectstack-ai`.
|
|
116
140
|
|
|
117
|
-
Skills
|
|
118
|
-
assistant catalog.
|
|
141
|
+
Skills are installed with `npx skills add objectstack-ai/objectstack/skills`
|
|
142
|
+
(see `AGENTS.md`) and also show up in the in-IDE assistant catalog.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# POSTGRES_PASSWORD / OS_AUTH_SECRET / OS_SECRET_KEY (generate secrets with
|
|
5
5
|
# `openssl rand -hex 32`), then `docker compose up -d`.
|
|
6
6
|
#
|
|
7
|
-
# Docs: https://
|
|
7
|
+
# Docs: https://objectstack.ai/docs/deployment/self-hosting
|
|
8
8
|
|
|
9
9
|
services:
|
|
10
10
|
app:
|
|
@@ -12,27 +12,31 @@ export default defineStack({
|
|
|
12
12
|
type: 'app',
|
|
13
13
|
name: 'Blank Starter',
|
|
14
14
|
description: 'Minimal ObjectStack environment — a clean slate for building.',
|
|
15
|
-
// Protocol compatibility range
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// Protocol compatibility range: the metadata-protocol major this app is
|
|
16
|
+
// authored against. The runtime checks it before it loads anything, so a
|
|
17
|
+
// runtime outside the range refuses this app at the boundary with the exact
|
|
18
|
+
// migration command instead of crashing later. Scaffolding stamped it to
|
|
19
|
+
// match the ObjectStack version you installed — change it when you
|
|
20
|
+
// deliberately move to a new protocol major, not to silence a mismatch.
|
|
21
|
+
// Guide: https://objectstack.ai/docs/upgrading
|
|
19
22
|
engines: { protocol: '^17' },
|
|
20
23
|
},
|
|
21
24
|
|
|
22
|
-
// `automation` backs flow execution and
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// without `automation` loaded they have nowhere to register and boot fails,
|
|
26
|
-
// keep this capability whenever `plugins:` lists a connector.
|
|
25
|
+
// `automation` backs flow execution and materializes any declarative
|
|
26
|
+
// `connectors:` entry into a live, dispatchable connector at boot. The
|
|
27
|
+
// connector executors below register their provider factories with it —
|
|
28
|
+
// without `automation` loaded they have nowhere to register and boot fails,
|
|
29
|
+
// so keep this capability whenever `plugins:` lists a connector.
|
|
27
30
|
requires: ['automation'],
|
|
28
31
|
|
|
29
|
-
// Generic connector executors
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// Security
|
|
34
|
-
//
|
|
32
|
+
// Generic connector executors, default-present so you can add a `connectors:`
|
|
33
|
+
// entry naming `provider: 'rest' | 'openapi' | 'mcp'` and have it materialize
|
|
34
|
+
// with zero host code. Zero-arg = contribute the provider factory only. Brand
|
|
35
|
+
// connectors (Slack, …) stay marketplace/opt-in.
|
|
36
|
+
// Security: a declarative `mcp` stdio transport spawns a local process from
|
|
37
|
+
// metadata, so it is denied by default — opt in per host with
|
|
35
38
|
// `new ConnectorMcpPlugin({ declarativeStdio: ['<trusted-command>'] })`.
|
|
39
|
+
// Authoring guide: https://objectstack.ai/docs/automation/connectors
|
|
36
40
|
plugins: [
|
|
37
41
|
new ConnectorRestPlugin(),
|
|
38
42
|
new ConnectorOpenApiPlugin(),
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
# An explicit EMPTY workspace: this project has no member packages, so
|
|
2
|
+
# this file is settings-only. The key is not decoration — pnpm 9.x and
|
|
3
|
+
# 10.0–10.4 parse this file BEFORE they read `engines`, and refuse a file
|
|
4
|
+
# without a `packages:` key outright ("ERROR packages field missing or
|
|
5
|
+
# empty") before resolving a single dependency.
|
|
6
|
+
# Not `packages: ['.']`: that would declare this project a workspace
|
|
7
|
+
# MEMBER — a monorepo root, which it is not.
|
|
8
|
+
packages: []
|
|
9
|
+
|
|
1
10
|
# pnpm does not run dependency install scripts unless they are approved here.
|
|
2
11
|
# Without this file a fresh `pnpm install` on pnpm 11 exits 1 with
|
|
3
12
|
# ERR_PNPM_IGNORED_BUILDS — pnpm 10 only warned, pnpm 11 made it a hard error.
|
|
4
13
|
#
|
|
5
14
|
# Both keys are needed; they are read by different pnpm versions:
|
|
6
|
-
# allowBuilds pnpm >= 10.
|
|
15
|
+
# allowBuilds pnpm >= 10.26 and pnpm 11+. pnpm 11 reads ONLY this
|
|
7
16
|
# one — onlyBuiltDependencies alone still errors.
|
|
8
|
-
# onlyBuiltDependencies pnpm 10.0–10.
|
|
17
|
+
# onlyBuiltDependencies pnpm 10.0–10.25, which do not understand allowBuilds.
|
|
9
18
|
#
|
|
10
19
|
# better-sqlite3 is the native sqlite driver (@objectstack/driver-sql's optional
|
|
11
20
|
# dependency); esbuild compiles objectstack.config.ts. Both ship prebuilt
|
|
@@ -21,3 +30,53 @@ onlyBuiltDependencies:
|
|
|
21
30
|
allowBuilds:
|
|
22
31
|
better-sqlite3: true
|
|
23
32
|
esbuild: true
|
|
33
|
+
|
|
34
|
+
# Some third-party peer ranges resolve outside what their declaring package
|
|
35
|
+
# states. `pnpm install` reports each one as an unmet peer — which would be the
|
|
36
|
+
# first thing a brand-new project shows you — and none is a real
|
|
37
|
+
# incompatibility:
|
|
38
|
+
#
|
|
39
|
+
# better-auth peers better-sqlite3 ^12.0.0 while the tree resolves 13.x. That
|
|
40
|
+
# peer is optional and covers handing better-auth a raw better-sqlite3
|
|
41
|
+
# `Database`; ObjectStack hands it an ObjectQL adapter instead, so nothing
|
|
42
|
+
# here goes down that path. Measured on the configuration the range does
|
|
43
|
+
# cover — better-auth's own Kysely dialect, running its migrations, sign-up,
|
|
44
|
+
# sign-in and adapter find/update/delete — 1.7.1 behaves identically on
|
|
45
|
+
# better-sqlite3 13.0.3 and on 12.11.1. The upstream range is stale.
|
|
46
|
+
#
|
|
47
|
+
# @better-auth/scim (held at a release candidate deliberately, not by
|
|
48
|
+
# neglect) peers better-call at an exact 1.3.7, while better-auth itself
|
|
49
|
+
# depends on 1.4.0. A better-auth plugin has to share the host's better-call
|
|
50
|
+
# instance, so the single 1.4.0 copy is the correct resolution rather than a
|
|
51
|
+
# skew to repair. This entry retires when SCIM moves off the rc: stable
|
|
52
|
+
# @better-auth/scim 1.7.1 already peers better-call 1.4.0.
|
|
53
|
+
#
|
|
54
|
+
# @better-auth/core, @better-auth/oauth-provider, @better-auth/scim and
|
|
55
|
+
# @better-auth/sso each peer an exact @better-auth/utils 0.4.2, while the
|
|
56
|
+
# tree hands them 0.5.0 — because better-call, better-auth's own HTTP layer,
|
|
57
|
+
# depends on ^0.5.0, and that is the copy your project's plugins resolve
|
|
58
|
+
# their peer against. Measured on the surface the range governs: those four
|
|
59
|
+
# import three symbols in total (base64/base64Url, createHash and, in core,
|
|
60
|
+
# createRandomStringGenerator), 0.5.0 declares all three unchanged, and on
|
|
61
|
+
# the inputs those call sites pass the two versions return identical values —
|
|
62
|
+
# checked again end to end through better-auth with the sso, oauth-provider
|
|
63
|
+
# and scim plugins, where both resolutions produce the same sign-up, sign-in,
|
|
64
|
+
# session, OAuth metadata, PKCE challenge and SCIM/SSO responses. Forcing
|
|
65
|
+
# utils back to 0.4.2 instead would push better-call off its own declared
|
|
66
|
+
# ^0.5.0 — trading four reported skews for one real one.
|
|
67
|
+
# Reported by pnpm 10.15–10.30; pnpm 10.31 changed peer resolution so all
|
|
68
|
+
# four land on 0.4.2 on their own. These four entries retire when the pnpm
|
|
69
|
+
# floor reaches 10.31, or when the four packages accept 0.5.0 upstream —
|
|
70
|
+
# NOT with the SCIM rc pin above, which the utils skew outlives.
|
|
71
|
+
#
|
|
72
|
+
# These suppress the report only: no resolution moves, and the lockfile is
|
|
73
|
+
# byte-identical with and without this block.
|
|
74
|
+
|
|
75
|
+
peerDependencyRules:
|
|
76
|
+
allowedVersions:
|
|
77
|
+
'better-auth>better-sqlite3': '13'
|
|
78
|
+
'@better-auth/scim>better-call': '1.4.0'
|
|
79
|
+
'@better-auth/core>@better-auth/utils': '0.5.0'
|
|
80
|
+
'@better-auth/oauth-provider>@better-auth/utils': '0.5.0'
|
|
81
|
+
'@better-auth/scim>@better-auth/utils': '0.5.0'
|
|
82
|
+
'@better-auth/sso>@better-auth/utils': '0.5.0'
|
|
@@ -21,8 +21,12 @@ export const Note = ObjectSchema.create({
|
|
|
21
21
|
}),
|
|
22
22
|
},
|
|
23
23
|
|
|
24
|
-
// Org-wide default (OWD): who can see records they don't own.
|
|
25
|
-
//
|
|
24
|
+
// Org-wide default (OWD): who can see records they don't own. `private` is
|
|
25
|
+
// owner-only until access is widened by a permission grant or a sharing rule.
|
|
26
|
+
// Declaring it is required, deliberately: `objectstack build` refuses an
|
|
27
|
+
// object that declares no OWD, so the baseline is always an authored decision
|
|
28
|
+
// rather than an accident. The other values, and how to widen access safely:
|
|
29
|
+
// https://objectstack.ai/docs/permissions/sharing-rules
|
|
26
30
|
sharingModel: 'private',
|
|
27
31
|
|
|
28
32
|
enable: {
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-objectstack",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.2.0",
|
|
4
4
|
"description": "Create a new ObjectStack project — npx create-objectstack",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-objectstack": "./bin/create-objectstack.js"
|
|
7
7
|
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./created-summary": {
|
|
10
|
+
"types": "./dist/created-summary.d.ts",
|
|
11
|
+
"import": "./dist/created-summary.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
8
14
|
"keywords": [
|
|
9
15
|
"objectstack",
|
|
10
16
|
"create",
|