create-objectstack 17.2.0 → 17.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,195 @@
1
1
  # create-objectstack
2
2
 
3
+ ## 17.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4998efa: Scaffolded projects now ship a CI workflow, and a `lint` script for it to run.
8
+ The blank template carries `.github/workflows/ci.yml` — one job, on `push` and
9
+ `pull_request`: checkout, pnpm, Node 22, `pnpm install --frozen-lockfile`, then
10
+ `pnpm validate`, `pnpm lint` and `pnpm typecheck`.
11
+
12
+ `lint` is new to the template (`objectstack lint`). It is not a second spelling
13
+ of `validate`: the two share the authoring-rule engine but only `lint` runs the
14
+ hook-body lowering check, which catches handlers that have silently stopped
15
+ lowering to metadata-only bodies — a change of deployment shape produced by a
16
+ refactor that looks like tidying.
17
+
18
+ The scaffolder already created `.github/` at runtime for a single file
19
+ (`copilot-instructions.md`) while the template's gates shipped as npm scripts
20
+ nothing ever ran, so a fresh project started with no CI at all — and ObjectStack
21
+ metadata mistakes fail silently at runtime, which makes `objectstack validate`
22
+ the only place they surface early. That gate is now unskippable for a human and
23
+ for an AI agent authoring metadata in the project, instead of advisory.
24
+
25
+ Existing projects are unaffected; copy the file from a fresh scaffold to adopt
26
+ it.
27
+ - 813d6c5: `npx create-objectstack` now declares the same TypeScript range as `os init` and
28
+ `os create`, and the value is generated rather than restated.
29
+
30
+ Three scaffolders write a new project's `package.json`, and the range that
31
+ decides whether that project type-checks at all had split: `os init` and
32
+ `os create` emitted `typescript: ^5.3.0` from a shared emission policy, while
33
+ this package's bundled template carried `^6.0.0`. Two projects created the same
34
+ day got different TypeScript **majors** depending on which documented entry
35
+ point the reader followed.
36
+
37
+ - **What changed for a scaffolded project.** Its declared `typescript`
38
+ devDependency floor moves from `^6.0.0` to `^5.3.0`. Both resolve to the same
39
+ installed compiler on a fresh install; what moves is the floor the project
40
+ **declares**, and a floor is a support promise. `^5.3.0` is the promise the
41
+ docs already make — "ObjectStack works with TypeScript 5.3+" on the getting
42
+ started page, "TypeScript 5.3.0 or later" in the deployment troubleshooting
43
+ page — and it is measured rather than assumed: TypeScript 5.3.3 type-checks
44
+ every shape these scaffolders emit with results identical to 6.0.3. The repo's
45
+ own `typescript@^6.0.3` devDependency is deliberately not this value; the same
46
+ doc sentence states both halves ("…but the project itself is built and tested
47
+ against TypeScript 6.x"). `engines.pnpm` was already in agreement and is now
48
+ held there by the same mechanism.
49
+ - **Why the value is generated.** This package cannot import from
50
+ `@objectstack/cli`: the dependency edge runs the other way, and the `npx`
51
+ package must not pull the CLI's package closure. So the values are stamped
52
+ into the bundled template at build time by
53
+ `scripts/sync-scaffold-emission-policy.mjs`, read out of the same
54
+ `SCAFFOLD_*` constants the other two scaffolders import, and
55
+ `pnpm check:scaffold-emission-policy` reddens the moment the inlined values
56
+ disagree with that source. Editing the two into agreement by hand would have
57
+ left them free to diverge again on the next move, silently, for the same
58
+ structural reason — which is how they diverged the first time.
59
+ - fd75728: Scaffolded projects now install the AI skills bundle for **one** agent runtime
60
+ instead of every runtime the skills CLI knows, so the bundle is committed once.
61
+
62
+ **Route B of the two the card offered was taken**, and the choice was measured
63
+ rather than argued. Against `skills@1.5.23` and the 11-skill catalog, the old
64
+ `--all` (shorthand for `--skill '*' --agent '*' -y`) wrote the same bundle to
65
+ three destinations — `.agents/` (46 real files, 604,102 B), `agent/` (46 real
66
+ files, 602,682 B, identical bodies with re-serialised frontmatter) and
67
+ `.claude/` (11 symlinks into `.agents/`). The template's `.gitignore` excluded
68
+ none of it, so a new project's first `git add -A` staged 22 `SKILL.md` paths
69
+ plus 11 symlinks. That reached the initial commit of a real app before anyone
70
+ noticed.
71
+
72
+ The scaffolder now runs
73
+ `npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y`,
74
+ which writes 46 real files to `.claude/skills/` and nothing else: 11 staged
75
+ `SKILL.md` paths, no symlinks, and a clone of that commit has readable skill
76
+ files on every platform.
77
+
78
+ Route A (keep `--all`, exclude the duplicates in the template `_gitignore`) was
79
+ built and cloned, not reasoned about, and both of its shapes were rejected.
80
+ Ignoring `.agents/` and `agent/` while committing `.claude/` gives a fresh
81
+ cloner 11 dangling symlinks and zero readable `SKILL.md`. Ignoring only
82
+ `agent/` works on POSIX but commits 11 symlinks that a `core.symlinks=false`
83
+ clone — git-for-Windows' default — materialises as ordinary files whose whole
84
+ content is the link target. `--all --copy`, the other way to make `.claude/`
85
+ real, fans out to 56 destination directories totalling 33.8 MB. A denylist is
86
+ also the wrong shape regardless of which paths it names: this package does not
87
+ choose the destination set, the skills CLI does, and it moves with that
88
+ package's releases.
89
+
90
+ The cost is the multi-runtime default, and it is paid in the open: the closing
91
+ summary now always prints an **AI Skills** block naming where the bundle landed
92
+ and the one-line command for any other runtime, one agent at a time. The
93
+ bundle is identical whichever agent is named.
94
+
95
+ Existing projects are unaffected. To shrink one that already carries the
96
+ triplicate, delete `.agents/` and `agent/` and re-run the single-agent command
97
+ above; `skills-lock.json` records source and hash, not paths, so it does not
98
+ change.
99
+
100
+ ## 17.3.0
101
+
102
+ ### Minor Changes
103
+
104
+ - 366f895: feat(auth): migrate `@better-auth/scim` from `1.7.0-rc.1` to stable `1.7.1` — the whole-model SCIM migration (#3653, epic #11632)
105
+
106
+ The stable line is the rc.2-lineage rewrite: the rc.1 `scimProvider` model,
107
+ `/scim/generate-token` endpoint and `storeSCIMToken` option no longer exist,
108
+ replaced by seven new models and a three-way connection contract. This lands
109
+ the migration atomically:
110
+
111
+ - **Seven new platform objects** back the stable models —
112
+ `sys_scim_connection_binding`, `sys_scim_group`, `sys_scim_group_member`,
113
+ `sys_scim_identity_tombstone`, `sys_scim_projection_grant`,
114
+ `sys_scim_subject`, `sys_scim_user` — bridged via `AUTH_MODEL_TO_PROTOCOL`,
115
+ registered in the platform-object-names registry, listed in
116
+ `BETTER_AUTH_MANAGED_OBJECTS`, and column-pinned by the parity gate (whose
117
+ `KNOWN_UNMAPPED_MODELS` shrinks to the empty set: the rc.1-era group
118
+ provisioning gap — IdP `/Groups` pushes hitting tables that did not exist —
119
+ is closed).
120
+ - **SCIM connections stay runtime data.** The stable constructor is satisfied
121
+ with an application-owned `authentication.verifyBearerToken` that resolves
122
+ the connection from a row at request time — not static boot config, and not
123
+ the upstream `managedConnections` catalog (deliberately not adopted).
124
+ - **ObjectStack owns SCIM credentials outright** (stable upstream stores no
125
+ credential at all): `sys_scim_connection_credential` plus
126
+ `scim-connection-service.ts` mint/digest/verify. At rest only an
127
+ HMAC-SHA-256 keyed by the deployment auth secret (base64url,
128
+ domain-separated) is stored — at parity or better than the rc.1 unsalted
129
+ SHA-256 — pinned by `credential-at-rest-posture.test.ts` including live
130
+ 401 paths for forged, revoked and expired bearers.
131
+ - **The ObjectQL better-auth adapter gains native transactions**
132
+ (`engine.transaction`, fail-closed on drivers without `beginTransaction`),
133
+ which stable scim requires by assertion for atomic provisioning writes.
134
+ - **Scaffold suppression retired**: the `@better-auth/scim>better-call`
135
+ `allowedVersions` entry (CLI renderer + blank template) is gone — stable
136
+ 1.7.1 peers `better-call@1.4.0` exactly — and its presence ratchets flipped
137
+ to absence pins. The `better-auth>better-sqlite3` and four
138
+ `@better-auth/utils` entries stay; their retirement conditions are separate
139
+ and unmet.
140
+ - The pin resolves **1.7.1 exactly** (not `^1.7.1`): 1.7.2 peers
141
+ `better-auth`/`@better-auth/core` at `^1.7.2`, which only the workspace
142
+ overrides' silencing would "satisfy" while the family is 1.7.1. Floating is
143
+ its own follow-up.
144
+
145
+ **Semver: minor, argued.** The rc.1 SCIM surface this replaces (generate-token
146
+ endpoint, rc.1 bearer tokens, `sys_scim_provider` rows) changes incompatibly —
147
+ but that surface is default-off (`OS_SCIM_ENABLED`), was shipped with a
148
+ documented "do not let the IdP push groups" boundary, and the maintainer ruled
149
+ (2026-08-25) that SCIM has no real customers and old data need not carry: the
150
+ one binding constraint is that an existing system upgrades smoothly, which it
151
+ does — every table the installed library can write exists at this version, and
152
+ SCIM-disabled deployments see no behavior change. A major would move the whole
153
+ fixed version group for a feature surface with zero consumers. Deployments
154
+ that had SCIM enabled must mint new connection credentials (digests are not
155
+ portable from rc.1 on any path — IdP token reissue is a migration-day
156
+ operator action regardless of semver level). `sys_scim_provider` itself is
157
+ NOT removed here; its retirement is tracked separately (#11757).
158
+
159
+ ### Patch Changes
160
+
161
+ - c6c7fec: fix(create-objectstack): stop reporting a failed pnpm probe as a deliberate npm choice (#11616)
162
+
163
+ `detectPackageManager()` was `try { execSync('pnpm --version') } catch { return
164
+ 'npm' }`, so every failure mode collapsed into one answer. `npm install` in the
165
+ scaffolder's output meant either *this machine has no pnpm* or *the probe
166
+ threw*, and nothing — no log line, no message — could tell the two apart.
167
+
168
+ That second case is reachable on an ordinary developer machine, not just in
169
+ theory: `pnpm --version` resolves through Corepack and therefore depends on the
170
+ directory it runs in. Measured on one machine, one binary, two directories —
171
+ `10.31.0` inside a repo that pins `packageManager`, `10.33.0` outside it, where
172
+ Corepack has to resolve, and may have to fetch, a version nothing pinned. A
173
+ user who has pnpm installed but is on a slow or offline network was silently
174
+ told to run npm.
175
+
176
+ The probe now reports why as well as what:
177
+
178
+ - `probe: 'ok'` — pnpm answered, so pnpm is used (unchanged, silent).
179
+ - `probe: 'absent'` — no pnpm on PATH at all, so npm is a real choice
180
+ (unchanged, silent).
181
+ - `probe: 'failed'` — pnpm **is** on PATH and the probe still threw. npm is
182
+ used exactly as before, and the run now says so, naming the underlying
183
+ failure: `pnpm is installed but \`pnpm --version\` failed (<reason>); using
184
+ npm as a fallback.`
185
+
186
+ **Which package manager a run uses is unchanged in all three cases** — it is
187
+ still pnpm if and only if the probe succeeded. The PATH lookup that separates
188
+ `absent` from `failed` runs only after the decision is already made and feeds
189
+ the message alone, so a miss there can change a warning's wording and never the
190
+ tool's behaviour. The only output that moves is one warning in a case that was
191
+ previously silent and wrong.
192
+
3
193
  ## 17.2.0
4
194
 
5
195
  ### Minor Changes
package/README.md CHANGED
@@ -56,9 +56,12 @@ you so rather than failing as an unknown template.
56
56
  every object name in the template is re-prefixed to match
57
57
  (`blank_note` → `my_app_note`).
58
58
  2. Installs dependencies (pnpm if available, otherwise npm).
59
- 3. Installs the ObjectStack AI skills bundle for coding agents
60
- (`npx skills add objectstack-ai/objectstack/skills --all` scoped to the
61
- curated `skills/` catalog).
59
+ 3. Installs the ObjectStack AI skills bundle for **one** coding agent —
60
+ `npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y`,
61
+ scoped to the curated `skills/` catalog. One agent, not `--all`, so the
62
+ bundle lands in exactly one directory (`.claude/skills/`) and a scaffolded
63
+ project commits it once; `src/skills-install.ts` carries the measurement.
64
+ The closing summary prints the same command for any other runtime.
62
65
  4. Writes `AGENTS.md` and `.github/copilot-instructions.md` with the project
63
66
  conventions — unless the template ships its own.
64
67
 
package/dist/index.js CHANGED
@@ -6,9 +6,57 @@ import {
6
6
  // src/index.ts
7
7
  import { Command } from "commander";
8
8
  import chalk2 from "chalk";
9
- import fs4 from "fs";
10
- import path4 from "path";
9
+ import fs5 from "fs";
10
+ import path5 from "path";
11
+ import { execSync as execSync2 } from "child_process";
12
+
13
+ // src/detect-package-manager.ts
14
+ import fs from "fs";
15
+ import path from "path";
11
16
  import { execSync } from "child_process";
17
+ function resolveOnPath(cmd, env = process.env) {
18
+ const raw = env.PATH ?? "";
19
+ if (!raw) return null;
20
+ const exts = process.platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
21
+ for (const dir of raw.split(path.delimiter)) {
22
+ if (!dir) continue;
23
+ for (const ext of exts) {
24
+ const candidate = path.join(dir, cmd + ext);
25
+ try {
26
+ if (fs.statSync(candidate).isFile()) return candidate;
27
+ } catch {
28
+ }
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function probeFailureDetail(err) {
34
+ const e = err ?? {};
35
+ if (e.signal) return `killed by ${e.signal}`;
36
+ const stderr = e.stderr == null ? "" : String(e.stderr);
37
+ const firstLine = stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
38
+ if (firstLine) return firstLine.length > 200 ? `${firstLine.slice(0, 197)}...` : firstLine;
39
+ if (typeof e.code === "string") return e.code;
40
+ if (typeof e.status === "number") return `exited ${e.status}`;
41
+ const msg = (e.message ?? "").split("\n")[0]?.trim();
42
+ return msg || "unknown error";
43
+ }
44
+ function defaultProbe() {
45
+ execSync("pnpm --version", { stdio: ["ignore", "ignore", "pipe"] });
46
+ }
47
+ function detectPackageManager(deps = {}) {
48
+ const probe = deps.probe ?? defaultProbe;
49
+ const pnpmOnPath = deps.pnpmOnPath ?? (() => resolveOnPath("pnpm") !== null);
50
+ try {
51
+ probe();
52
+ return { pm: "pnpm", probe: "ok" };
53
+ } catch (err) {
54
+ if (!pnpmOnPath()) return { pm: "npm", probe: "absent" };
55
+ return { pm: "npm", probe: "failed", detail: probeFailureDetail(err) };
56
+ }
57
+ }
58
+
59
+ // src/index.ts
12
60
  import { fileURLToPath } from "url";
13
61
 
14
62
  // src/pkg-utils.ts
@@ -25,41 +73,41 @@ function syncObjectStackDeps(pkg, version) {
25
73
  }
26
74
 
27
75
  // src/template-copy.ts
28
- import fs from "fs";
29
- import path from "path";
76
+ import fs2 from "fs";
77
+ import path2 from "path";
30
78
  var TEMPLATE_FILE_ALIASES = /* @__PURE__ */ new Map([
31
79
  ["_gitignore", ".gitignore"]
32
80
  ]);
33
81
  function copyDir(src, dest, collected, rel = "") {
34
- fs.mkdirSync(dest, { recursive: true });
35
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
36
- const srcPath = path.join(src, entry.name);
82
+ fs2.mkdirSync(dest, { recursive: true });
83
+ for (const entry of fs2.readdirSync(src, { withFileTypes: true })) {
84
+ const srcPath = path2.join(src, entry.name);
37
85
  const outName = entry.isFile() ? TEMPLATE_FILE_ALIASES.get(entry.name) ?? entry.name : entry.name;
38
- const destPath = path.join(dest, outName);
86
+ const destPath = path2.join(dest, outName);
39
87
  const relPath = rel ? `${rel}/${outName}` : outName;
40
88
  if (entry.isDirectory()) {
41
89
  copyDir(srcPath, destPath, collected, relPath);
42
90
  } else if (entry.isFile()) {
43
- fs.copyFileSync(srcPath, destPath);
91
+ fs2.copyFileSync(srcPath, destPath);
44
92
  collected.push(relPath);
45
93
  }
46
94
  }
47
95
  }
48
96
 
49
97
  // src/rewrite-identity.ts
50
- import fs2 from "fs";
51
- import path2 from "path";
98
+ import fs3 from "fs";
99
+ import path3 from "path";
52
100
  var CONFIG_NAMESPACE_RE = /\bnamespace:\s*(['"`])([a-z0-9_]+)\1/i;
53
101
  function readTemplateNamespace(targetDir) {
54
- const configPath = path2.join(targetDir, "objectstack.config.ts");
55
- if (fs2.existsSync(configPath)) {
56
- const m = CONFIG_NAMESPACE_RE.exec(fs2.readFileSync(configPath, "utf8"));
102
+ const configPath = path3.join(targetDir, "objectstack.config.ts");
103
+ if (fs3.existsSync(configPath)) {
104
+ const m = CONFIG_NAMESPACE_RE.exec(fs3.readFileSync(configPath, "utf8"));
57
105
  if (m) return m[2];
58
106
  }
59
- const manifestPath = path2.join(targetDir, "objectstack.manifest.json");
60
- if (fs2.existsSync(manifestPath)) {
107
+ const manifestPath = path3.join(targetDir, "objectstack.manifest.json");
108
+ if (fs3.existsSync(manifestPath)) {
61
109
  try {
62
- const m = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
110
+ const m = JSON.parse(fs3.readFileSync(manifestPath, "utf8"));
63
111
  if (typeof m.namespace === "string" && m.namespace) return m.namespace;
64
112
  } catch {
65
113
  }
@@ -67,9 +115,9 @@ function readTemplateNamespace(targetDir) {
67
115
  return void 0;
68
116
  }
69
117
  function tsFiles(dir, out = []) {
70
- if (!fs2.existsSync(dir)) return out;
71
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
72
- const full = path2.join(dir, entry.name);
118
+ if (!fs3.existsSync(dir)) return out;
119
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
120
+ const full = path3.join(dir, entry.name);
73
121
  if (entry.isDirectory()) {
74
122
  if (entry.name === "node_modules") continue;
75
123
  tsFiles(full, out);
@@ -83,7 +131,7 @@ var namePrefixRe = (ns, flags) => new RegExp(`(\\bname:\\s*)(['"\`])${ns}_([a-z0
83
131
  function rewriteObjectNamePrefix(dir, from, to) {
84
132
  let rewritten = 0;
85
133
  for (const file of tsFiles(dir)) {
86
- const before = fs2.readFileSync(file, "utf8");
134
+ const before = fs3.readFileSync(file, "utf8");
87
135
  const after = before.replace(
88
136
  namePrefixRe(from, "g"),
89
137
  (_m, prefix, q, rest) => {
@@ -91,7 +139,7 @@ function rewriteObjectNamePrefix(dir, from, to) {
91
139
  return `${prefix}${q}${to}_${rest}${q}`;
92
140
  }
93
141
  );
94
- if (after !== before) fs2.writeFileSync(file, after);
142
+ if (after !== before) fs3.writeFileSync(file, after);
95
143
  }
96
144
  return rewritten;
97
145
  }
@@ -99,11 +147,11 @@ function findStaleNamespacePrefixes(dir, oldNs) {
99
147
  const re = namePrefixRe(oldNs, "");
100
148
  const stale = [];
101
149
  for (const file of tsFiles(dir)) {
102
- const lines = fs2.readFileSync(file, "utf8").split("\n");
150
+ const lines = fs3.readFileSync(file, "utf8").split("\n");
103
151
  for (let i = 0; i < lines.length; i++) {
104
152
  if (re.test(lines[i])) {
105
153
  stale.push({
106
- file: path2.relative(dir, file),
154
+ file: path3.relative(dir, file),
107
155
  line: i + 1,
108
156
  text: lines[i].trim()
109
157
  });
@@ -138,13 +186,13 @@ function templateNames() {
138
186
  }
139
187
 
140
188
  // src/runtime-image.ts
141
- import fs3 from "fs";
142
- import path3 from "path";
189
+ import fs4 from "fs";
190
+ import path4 from "path";
143
191
  var RUNTIME_FROM_RE = /^FROM ghcr\.io\/objectstack-ai\/objectstack:([A-Za-z0-9_][A-Za-z0-9_.+-]*)[ \t]*$/;
144
192
  var PINNABLE_VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
145
193
  var PROSE_COMMENT_RE = /^#[ \t]+\S/;
146
194
  function readResolvedCliVersion(targetDir) {
147
- const pkgPath = path3.join(
195
+ const pkgPath = path4.join(
148
196
  targetDir,
149
197
  "node_modules",
150
198
  "@objectstack",
@@ -152,7 +200,7 @@ function readResolvedCliVersion(targetDir) {
152
200
  "package.json"
153
201
  );
154
202
  try {
155
- const version = JSON.parse(fs3.readFileSync(pkgPath, "utf8")).version;
203
+ const version = JSON.parse(fs4.readFileSync(pkgPath, "utf8")).version;
156
204
  return typeof version === "string" && PINNABLE_VERSION_RE.test(version) ? version : void 0;
157
205
  } catch {
158
206
  return void 0;
@@ -170,10 +218,10 @@ function pinRuntimeImage(targetDir, version) {
170
218
  if (!PINNABLE_VERSION_RE.test(version)) {
171
219
  return { pinned: false, reason: `'${version}' is not a pinnable version` };
172
220
  }
173
- const dockerfile = path3.join(targetDir, "Dockerfile");
221
+ const dockerfile = path4.join(targetDir, "Dockerfile");
174
222
  let text;
175
223
  try {
176
- text = fs3.readFileSync(dockerfile, "utf8");
224
+ text = fs4.readFileSync(dockerfile, "utf8");
177
225
  } catch {
178
226
  return { pinned: false, reason: "no Dockerfile in the scaffolded project" };
179
227
  }
@@ -193,10 +241,23 @@ function pinRuntimeImage(targetDir, version) {
193
241
  if (!check || check[1] !== version) {
194
242
  return { pinned: false, reason: "rewrite did not produce the pinned tag" };
195
243
  }
196
- fs3.writeFileSync(dockerfile, pinnedText);
244
+ fs4.writeFileSync(dockerfile, pinnedText);
197
245
  return { pinned: true, tag: version };
198
246
  }
199
247
 
248
+ // src/skills-install.ts
249
+ var SKILLS_CATALOG = "objectstack-ai/objectstack/skills";
250
+ var DEFAULT_SKILLS_AGENT = "claude-code";
251
+ var DEFAULT_SKILLS_DIR = ".claude/skills/";
252
+ var EXAMPLE_OTHER_AGENT = "codex";
253
+ function skillsAddArgs(agent) {
254
+ return `skills add ${SKILLS_CATALOG} --skill '*' --agent ${agent} -y`;
255
+ }
256
+ var SKILLS_INSTALL_COMMAND = `npx -y ${skillsAddArgs(DEFAULT_SKILLS_AGENT)}`;
257
+ function skillsInstallHint(agent) {
258
+ return `npx ${skillsAddArgs(agent)}`;
259
+ }
260
+
200
261
  // src/banner.ts
201
262
  import chalk from "chalk";
202
263
  var PREFIX = " \u25C6 Create ObjectStack ";
@@ -219,8 +280,8 @@ function renderVersionBanner(version) {
219
280
 
220
281
  // src/index.ts
221
282
  var __filename2 = fileURLToPath(import.meta.url);
222
- var __dirname2 = path4.dirname(__filename2);
223
- var BUNDLED_TEMPLATES_DIR = path4.resolve(__dirname2, "templates");
283
+ var __dirname2 = path5.dirname(__filename2);
284
+ var BUNDLED_TEMPLATES_DIR = path5.resolve(__dirname2, "templates");
224
285
  function toTitleCase(str) {
225
286
  return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
226
287
  }
@@ -237,8 +298,8 @@ function sanitizeNamespace(name) {
237
298
  }
238
299
  function readCliVersion() {
239
300
  try {
240
- const pkgPath = path4.resolve(__dirname2, "..", "package.json");
241
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
301
+ const pkgPath = path5.resolve(__dirname2, "..", "package.json");
302
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf8"));
242
303
  return String(pkg.version || "0.0.0");
243
304
  } catch {
244
305
  return "0.0.0";
@@ -264,17 +325,9 @@ function printStep(msg) {
264
325
  function printWarning(msg) {
265
326
  console.log(chalk2.yellow(` \u26A0 ${msg}`));
266
327
  }
267
- function detectPackageManager() {
268
- try {
269
- execSync("pnpm --version", { stdio: "ignore" });
270
- return "pnpm";
271
- } catch {
272
- return "npm";
273
- }
274
- }
275
328
  function loadBundled(templateDir, targetDir) {
276
- const src = path4.join(BUNDLED_TEMPLATES_DIR, templateDir);
277
- if (!fs4.existsSync(src)) {
329
+ const src = path5.join(BUNDLED_TEMPLATES_DIR, templateDir);
330
+ if (!fs5.existsSync(src)) {
278
331
  throw new Error(`Bundled template missing on disk: ${src}`);
279
332
  }
280
333
  const collected = [];
@@ -284,39 +337,39 @@ function loadBundled(templateDir, targetDir) {
284
337
  function rewriteProjectIdentity(targetDir, projectName, namespace) {
285
338
  const title = toTitleCase(projectName);
286
339
  const templateNamespace = readTemplateNamespace(targetDir);
287
- const pkgPath = path4.join(targetDir, "package.json");
288
- if (fs4.existsSync(pkgPath)) {
340
+ const pkgPath = path5.join(targetDir, "package.json");
341
+ if (fs5.existsSync(pkgPath)) {
289
342
  try {
290
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
343
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf8"));
291
344
  pkg.name = projectName;
292
345
  syncObjectStackDeps(pkg, readCliVersion());
293
- fs4.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
346
+ fs5.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
294
347
  } catch {
295
348
  }
296
349
  }
297
- const manifestPath = path4.join(targetDir, "objectstack.manifest.json");
298
- if (fs4.existsSync(manifestPath)) {
350
+ const manifestPath = path5.join(targetDir, "objectstack.manifest.json");
351
+ if (fs5.existsSync(manifestPath)) {
299
352
  try {
300
- const m = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
353
+ const m = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
301
354
  m.name = projectName;
302
355
  m.displayName = title;
303
356
  if ("namespace" in m) m.namespace = namespace;
304
357
  delete m.description;
305
- fs4.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
358
+ fs5.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
306
359
  } catch {
307
360
  }
308
361
  }
309
- const configPath = path4.join(targetDir, "objectstack.config.ts");
310
- if (fs4.existsSync(configPath)) {
311
- let cfg = fs4.readFileSync(configPath, "utf8");
362
+ const configPath = path5.join(targetDir, "objectstack.config.ts");
363
+ if (fs5.existsSync(configPath)) {
364
+ let cfg = fs5.readFileSync(configPath, "utf8");
312
365
  cfg = cfg.replace(/(\bid:\s*)(['"`])[^'"`]*\2/, `$1$2${projectName}$2`);
313
366
  cfg = cfg.replace(/(\bnamespace:\s*)(['"`])[^'"`]*\2/, `$1$2${namespace}$2`);
314
367
  cfg = cfg.replace(/(\bname:\s*)(['"`])[^'"`]*\2/, `$1$2${title}$2`);
315
368
  cfg = cfg.replace(/^[ \t]*description:\s*(['"`])[^'"`]*\1,?\r?\n/m, "");
316
- fs4.writeFileSync(configPath, cfg);
369
+ fs5.writeFileSync(configPath, cfg);
317
370
  }
318
371
  if (templateNamespace && namespace !== templateNamespace) {
319
- const srcDir = path4.join(targetDir, "src");
372
+ const srcDir = path5.join(targetDir, "src");
320
373
  rewriteObjectNamePrefix(srcDir, templateNamespace, namespace);
321
374
  const stale = findStaleNamespacePrefixes(srcDir, templateNamespace);
322
375
  if (stale.length > 0) {
@@ -330,32 +383,32 @@ The generated project would fail 'objectstack build' on the \${namespace}_\${sho
330
383
  );
331
384
  }
332
385
  }
333
- const readmePath = path4.join(targetDir, "README.md");
334
- if (fs4.existsSync(readmePath)) {
335
- let md = fs4.readFileSync(readmePath, "utf8");
386
+ const readmePath = path5.join(targetDir, "README.md");
387
+ if (fs5.existsSync(readmePath)) {
388
+ let md = fs5.readFileSync(readmePath, "utf8");
336
389
  md = md.replace(/^#\s+.*$/m, `# ${title}`);
337
- fs4.writeFileSync(readmePath, md);
390
+ fs5.writeFileSync(readmePath, md);
338
391
  }
339
392
  writeAgentGuides(targetDir, title, projectName);
340
393
  }
341
394
  function writeAgentGuides(targetDir, title, projectName) {
342
- const templatePath = path4.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
395
+ const templatePath = path5.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
343
396
  let template;
344
397
  try {
345
- template = fs4.readFileSync(templatePath, "utf8");
398
+ template = fs5.readFileSync(templatePath, "utf8");
346
399
  } catch (err) {
347
400
  if (err?.code === "ENOENT") return;
348
401
  throw err;
349
402
  }
350
403
  const rendered = template.replace(/\{\{PROJECT_TITLE\}\}/g, title).replace(/\{\{PROJECT_NAME\}\}/g, projectName);
351
- writeIfAbsent(path4.join(targetDir, "AGENTS.md"), rendered);
352
- const copilotPath = path4.join(targetDir, ".github", "copilot-instructions.md");
353
- fs4.mkdirSync(path4.dirname(copilotPath), { recursive: true });
404
+ writeIfAbsent(path5.join(targetDir, "AGENTS.md"), rendered);
405
+ const copilotPath = path5.join(targetDir, ".github", "copilot-instructions.md");
406
+ fs5.mkdirSync(path5.dirname(copilotPath), { recursive: true });
354
407
  writeIfAbsent(copilotPath, rendered);
355
408
  }
356
409
  function topLevelNames(dir) {
357
410
  try {
358
- return new Set(fs4.readdirSync(dir));
411
+ return new Set(fs5.readdirSync(dir));
359
412
  } catch {
360
413
  return /* @__PURE__ */ new Set();
361
414
  }
@@ -398,7 +451,7 @@ function printCreatedSummary(targetDir, opts) {
398
451
  }
399
452
  function writeIfAbsent(filePath, contents) {
400
453
  try {
401
- fs4.writeFileSync(filePath, contents, { flag: "wx" });
454
+ fs5.writeFileSync(filePath, contents, { flag: "wx" });
402
455
  } catch (err) {
403
456
  if (err?.code !== "EEXIST") throw err;
404
457
  }
@@ -428,18 +481,25 @@ var program = new Command().name("create-objectstack").description("Create a new
428
481
  }
429
482
  const template = lookup.template;
430
483
  const cwd = process.cwd();
431
- const projectName = name || path4.basename(cwd);
484
+ const projectName = name || path5.basename(cwd);
432
485
  const namespace = sanitizeNamespace(projectName);
433
- const targetDir = name ? path4.resolve(cwd, name) : cwd;
486
+ const targetDir = name ? path5.resolve(cwd, name) : cwd;
434
487
  const isCurrentDir = targetDir === cwd;
435
- const pm = detectPackageManager();
488
+ const detected = detectPackageManager();
489
+ const pm = detected.pm;
490
+ if (detected.probe === "failed") {
491
+ printWarning(
492
+ `pnpm is installed but \`pnpm --version\` failed (${detected.detail}); using npm as a fallback. The commands below name npm because the probe did not answer, not because this project prefers it.`
493
+ );
494
+ console.log("");
495
+ }
436
496
  printKV("Environment", projectName);
437
497
  printKV("Namespace", namespace);
438
498
  printKV("Template", `${options.template} \u2014 ${template.description}`);
439
499
  printKV("Directory", targetDir);
440
500
  console.log("");
441
- if (!isCurrentDir && fs4.existsSync(targetDir)) {
442
- const existing = fs4.readdirSync(targetDir);
501
+ if (!isCurrentDir && fs5.existsSync(targetDir)) {
502
+ const existing = fs5.readdirSync(targetDir);
443
503
  if (existing.length > 0) {
444
504
  printError(`Directory already exists and is not empty: ${targetDir}`);
445
505
  process.exit(1);
@@ -447,7 +507,7 @@ var program = new Command().name("create-objectstack").description("Create a new
447
507
  }
448
508
  const targetWasEmpty = topLevelNames(targetDir).size === 0;
449
509
  try {
450
- fs4.mkdirSync(targetDir, { recursive: true });
510
+ fs5.mkdirSync(targetDir, { recursive: true });
451
511
  const createdFiles = loadBundled(template.source.dir, targetDir);
452
512
  rewriteProjectIdentity(targetDir, projectName, namespace);
453
513
  printSuccess(`Template files written (${createdFiles.length})`);
@@ -456,7 +516,7 @@ var program = new Command().name("create-objectstack").description("Create a new
456
516
  printStep("Installing dependencies...");
457
517
  let installed = false;
458
518
  try {
459
- execSync(`${pm} install`, { stdio: "inherit", cwd: targetDir });
519
+ execSync2(`${pm} install`, { stdio: "inherit", cwd: targetDir });
460
520
  installed = true;
461
521
  console.log("");
462
522
  } catch {
@@ -479,17 +539,20 @@ var program = new Command().name("create-objectstack").description("Create a new
479
539
  }
480
540
  }
481
541
  const beforeSkills = topLevelNames(targetDir);
542
+ let skillsInstalled = false;
482
543
  if (!options.skipInstall && !options.skipSkills) {
483
544
  printStep("Installing AI skills for your coding agent...");
484
545
  try {
485
- execSync("npx -y skills add objectstack-ai/objectstack/skills --all", {
546
+ execSync2(SKILLS_INSTALL_COMMAND, {
486
547
  stdio: "inherit",
487
548
  cwd: targetDir
488
549
  });
550
+ skillsInstalled = true;
489
551
  console.log("");
490
552
  } catch {
491
553
  printWarning(
492
- "Skills installation skipped. Run manually:\n npx skills add objectstack-ai/objectstack/skills"
554
+ `Skills installation skipped. Run manually:
555
+ ${skillsInstallHint(DEFAULT_SKILLS_AGENT)}`
493
556
  );
494
557
  console.log("");
495
558
  }
@@ -513,10 +576,23 @@ var program = new Command().name("create-objectstack").description("Create a new
513
576
  console.log(chalk2.dim(` ${devLabel.padEnd(labelWidth)}# Start development server`));
514
577
  console.log(chalk2.dim(` ${validateLabel.padEnd(labelWidth)}# Verify metadata: schema + predicates + bindings`));
515
578
  console.log(chalk2.dim(` ${" ".repeat(labelWidth)}# (run after every metadata edit \u2014 see AGENTS.md)`));
516
- if (options.skipInstall || options.skipSkills) {
517
- console.log("");
579
+ console.log("");
580
+ if (skillsInstalled) {
581
+ console.log(chalk2.bold(" AI Skills:"));
582
+ console.log(
583
+ chalk2.dim(
584
+ ` Installed for ${DEFAULT_SKILLS_AGENT} \u2192 ${DEFAULT_SKILLS_DIR} \u2014 commit it with your project.`
585
+ )
586
+ );
587
+ console.log(chalk2.dim(" Using a different agent? Add it one runtime at a time:"));
588
+ console.log(chalk2.dim(` ${skillsInstallHint(EXAMPLE_OTHER_AGENT)}`));
589
+ console.log(chalk2.dim(" (`npx skills add --help` lists every agent name)"));
590
+ } else {
518
591
  console.log(chalk2.bold(" AI Skills (recommended):"));
519
- console.log(chalk2.dim(" npx skills add objectstack-ai/objectstack/skills"));
592
+ console.log(chalk2.dim(` ${skillsInstallHint(DEFAULT_SKILLS_AGENT)}`));
593
+ console.log(
594
+ chalk2.dim(` Replace \`${DEFAULT_SKILLS_AGENT}\` with your own agent runtime if it differs.`)
595
+ );
520
596
  }
521
597
  console.log("");
522
598
  } catch (error) {
@@ -73,13 +73,19 @@ Run `npm run build` when you need the compiled `dist/objectstack.json` artifact.
73
73
 
74
74
  ## AI Skills
75
75
 
76
- This project uses ObjectStack skills from `objectstack-ai/objectstack`.
77
- Install or update skills with the standard [skills CLI](https://skills.sh/):
76
+ This project uses ObjectStack skills from `objectstack-ai/objectstack`. They
77
+ were installed for one agent runtime, live in `.claude/skills/`, and are
78
+ committed with the project. Install or update them with the standard
79
+ [skills CLI](https://skills.sh/), naming your runtime:
78
80
 
79
81
  ```bash
80
- npx skills add objectstack-ai/objectstack/skills
82
+ npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y
81
83
  ```
82
84
 
85
+ Adding a second runtime? Re-run it with that runtime's `--agent` name.
86
+ ⛔ Do not use `--all`: it writes the same bundle to `.agents/`, `agent/` and
87
+ `.claude/` at once, and all three then land in your next commit.
88
+
83
89
  Skills are triggered automatically based on task context:
84
90
 
85
91
  | Skill | Trigger Context |
@@ -0,0 +1,67 @@
1
+ # Continuous integration for this ObjectStack app.
2
+ #
3
+ # ObjectStack metadata fails SILENTLY at runtime, not at edit time: a mistyped
4
+ # permission grant or a bare field name in an action predicate raises nothing,
5
+ # it just behaves wrongly for whoever hits it first. `objectstack validate` is
6
+ # where that class surfaces — and running it is something a human, or an AI
7
+ # agent authoring metadata in this project, has to remember. This workflow is
8
+ # what makes it unskippable. AGENTS.md carries the authoring conventions it
9
+ # enforces.
10
+ #
11
+ # One job, one file: a starting point, not a CI framework. Grow it with the
12
+ # project — add a step when you add a script, a second job when you deploy.
13
+
14
+ name: CI
15
+
16
+ on: [push, pull_request]
17
+
18
+ # Read-only. Nothing here writes to the repository or publishes anything.
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ verify:
24
+ name: Validate
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 15
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+
30
+ # pnpm BEFORE setup-node, deliberately. `cache: pnpm` below makes
31
+ # setup-node shell out to pnpm to locate the store, so pnpm has to be on
32
+ # PATH by then. Reversed, this does not degrade — the job dies in the
33
+ # setup step with "Unable to locate executable file: pnpm".
34
+ #
35
+ # `version` is explicit because this project declares no `packageManager`
36
+ # field — pinning one would make the project pnpm-only, and a
37
+ # corepack-driven npm or yarn then refuses to run in it — so
38
+ # pnpm/action-setup has nothing to resolve from. Keep this in step with
39
+ # the `engines.pnpm` floor in package.json.
40
+ - uses: pnpm/action-setup@v6
41
+ with:
42
+ version: 10
43
+
44
+ - uses: actions/setup-node@v7
45
+ with:
46
+ node-version: '22'
47
+ cache: pnpm
48
+
49
+ # `--frozen-lockfile` installs exactly what the lockfile records and fails
50
+ # when the two disagree, so `pnpm-lock.yaml` has to be committed.
51
+ # Scaffolding wrote it for you unless you passed `--skip-install`; in that
52
+ # case run `pnpm install` once and commit the result.
53
+ - run: pnpm install --frozen-lockfile
54
+
55
+ # The gate this file exists for: schema, CEL predicates and widget
56
+ # bindings. The same checks `pnpm build` runs, without producing an
57
+ # artifact.
58
+ - run: pnpm validate
59
+
60
+ # Not a second spelling of `validate`. The two share the authoring-rule
61
+ # engine but not the rule set — the hook-body lowering check, which
62
+ # catches handlers that silently stop lowering to metadata-only bodies
63
+ # (a change of deployment shape, from a refactor that looks like
64
+ # tidying), runs here and nowhere else.
65
+ - run: pnpm lint
66
+
67
+ - run: pnpm typecheck
@@ -100,6 +100,8 @@ After editing any metadata, run:
100
100
 
101
101
  ```bash
102
102
  pnpm validate # schema + CEL predicates + widget bindings (no artifact)
103
+ pnpm lint # authoring rules validate does not run — e.g. a handler that
104
+ # stopped lowering to a metadata-only body
103
105
  pnpm typecheck # TypeScript types against @objectstack/spec
104
106
  ```
105
107
 
@@ -138,5 +140,10 @@ covered in [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-
138
140
  - Add a flow or automation: see `objectstack-automation`.
139
141
  - Add an AI agent: see `objectstack-ai`.
140
142
 
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.
143
+ Skills live in `.claude/skills/` and are committed with this project, once.
144
+ They are installed one agent runtime at a time (see `AGENTS.md`), and also show
145
+ up in the in-IDE assistant catalog:
146
+
147
+ ```bash
148
+ npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y
149
+ ```
@@ -11,6 +11,7 @@
11
11
  "start": "objectstack start",
12
12
  "build": "objectstack build",
13
13
  "validate": "objectstack validate",
14
+ "lint": "objectstack lint",
14
15
  "typecheck": "tsc --noEmit"
15
16
  },
16
17
  "dependencies": {
@@ -23,6 +24,6 @@
23
24
  },
24
25
  "devDependencies": {
25
26
  "@objectstack/cli": "^17.0.0",
26
- "typescript": "^6.0.0"
27
+ "typescript": "^5.3.0"
27
28
  }
28
29
  }
@@ -44,12 +44,9 @@ allowBuilds:
44
44
  # sign-in and adapter find/update/delete — 1.7.1 behaves identically on
45
45
  # better-sqlite3 13.0.3 and on 12.11.1. The upstream range is stale.
46
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.
47
+ # (The '@better-auth/scim>better-call' entry that used to sit here retired
48
+ # with the scim release-candidate pin stable @better-auth/scim 1.7.1 peers
49
+ # better-call 1.4.0 exactly, so the skew it declared away is gone.)
53
50
  #
54
51
  # @better-auth/core, @better-auth/oauth-provider, @better-auth/scim and
55
52
  # @better-auth/sso each peer an exact @better-auth/utils 0.4.2, while the
@@ -75,7 +72,6 @@ allowBuilds:
75
72
  peerDependencyRules:
76
73
  allowedVersions:
77
74
  'better-auth>better-sqlite3': '13'
78
- '@better-auth/scim>better-call': '1.4.0'
79
75
  '@better-auth/core>@better-auth/utils': '0.5.0'
80
76
  '@better-auth/oauth-provider>@better-auth/utils': '0.5.0'
81
77
  '@better-auth/scim>@better-auth/utils': '0.5.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-objectstack",
3
- "version": "17.2.0",
3
+ "version": "17.4.0",
4
4
  "description": "Create a new ObjectStack project — npx create-objectstack",
5
5
  "bin": {
6
6
  "create-objectstack": "./bin/create-objectstack.js"
@@ -28,7 +28,8 @@
28
28
  "@types/node": "^26.2.0",
29
29
  "tsup": "^8.5.1",
30
30
  "typescript": "^6.0.3",
31
- "vitest": "^4.1.10"
31
+ "vitest": "^4.1.10",
32
+ "yaml": "^2.9.0"
32
33
  },
33
34
  "repository": {
34
35
  "type": "git",
@@ -49,7 +50,7 @@
49
50
  "node": ">=22.0.0"
50
51
  },
51
52
  "scripts": {
52
- "build": "tsup",
53
+ "build": "node ../../scripts/sync-scaffold-emission-policy.mjs && tsup && node ../../scripts/check-dts-emitted.mjs",
53
54
  "typecheck": "tsc --noEmit",
54
55
  "dev": "tsup --watch",
55
56
  "test": "vitest run"