create-objectstack 15.1.1 → 16.0.0-rc.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/README.md CHANGED
@@ -59,7 +59,8 @@ offline.
59
59
  (`blank_note` → `my_app_note`).
60
60
  2. Installs dependencies (pnpm if available, otherwise npm).
61
61
  3. Installs the ObjectStack AI skills bundle for coding agents
62
- (`npx skills add objectstack-ai/framework --all`).
62
+ (`npx skills add objectstack-ai/framework/skills --all` — scoped to the
63
+ curated `skills/` catalog).
63
64
  4. Writes `AGENTS.md` and `.github/copilot-instructions.md` with the project
64
65
  conventions — unless the template ships its own.
65
66
 
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.ts
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
- import fs from "fs";
5
- import path from "path";
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
6
  import os from "os";
7
7
  import { execSync } from "child_process";
8
8
  import { fileURLToPath } from "url";
@@ -25,10 +25,32 @@ function syncObjectStackDeps(pkg, version) {
25
25
  }
26
26
  }
27
27
 
28
+ // src/template-copy.ts
29
+ import fs from "fs";
30
+ import path from "path";
31
+ var TEMPLATE_FILE_ALIASES = /* @__PURE__ */ new Map([
32
+ ["_gitignore", ".gitignore"]
33
+ ]);
34
+ function copyDir(src, dest, collected, rel = "") {
35
+ fs.mkdirSync(dest, { recursive: true });
36
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
37
+ const srcPath = path.join(src, entry.name);
38
+ const outName = entry.isFile() ? TEMPLATE_FILE_ALIASES.get(entry.name) ?? entry.name : entry.name;
39
+ const destPath = path.join(dest, outName);
40
+ const relPath = rel ? `${rel}/${outName}` : outName;
41
+ if (entry.isDirectory()) {
42
+ copyDir(srcPath, destPath, collected, relPath);
43
+ } else if (entry.isFile()) {
44
+ fs.copyFileSync(srcPath, destPath);
45
+ collected.push(relPath);
46
+ }
47
+ }
48
+ }
49
+
28
50
  // src/index.ts
29
51
  var __filename2 = fileURLToPath(import.meta.url);
30
- var __dirname2 = path.dirname(__filename2);
31
- var BUNDLED_TEMPLATES_DIR = path.resolve(__dirname2, "templates");
52
+ var __dirname2 = path2.dirname(__filename2);
53
+ var BUNDLED_TEMPLATES_DIR = path2.resolve(__dirname2, "templates");
32
54
  var REMOTE_REPO = "objectstack-ai/templates";
33
55
  var REMOTE_BRANCH = "main";
34
56
  var REMOTE_TARBALL_URL = `https://codeload.github.com/${REMOTE_REPO}/tar.gz/refs/heads/${REMOTE_BRANCH}`;
@@ -74,8 +96,8 @@ function sanitizeNamespace(name) {
74
96
  }
75
97
  function readCliVersion() {
76
98
  try {
77
- const pkgPath = path.resolve(__dirname2, "..", "package.json");
78
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
99
+ const pkgPath = path2.resolve(__dirname2, "..", "package.json");
100
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
79
101
  return String(pkg.version || "0.0.0");
80
102
  } catch {
81
103
  return "0.0.0";
@@ -109,23 +131,9 @@ function detectPackageManager() {
109
131
  return "npm";
110
132
  }
111
133
  }
112
- function copyDir(src, dest, collected, rel = "") {
113
- fs.mkdirSync(dest, { recursive: true });
114
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
115
- const srcPath = path.join(src, entry.name);
116
- const destPath = path.join(dest, entry.name);
117
- const relPath = rel ? `${rel}/${entry.name}` : entry.name;
118
- if (entry.isDirectory()) {
119
- copyDir(srcPath, destPath, collected, relPath);
120
- } else if (entry.isFile()) {
121
- fs.copyFileSync(srcPath, destPath);
122
- collected.push(relPath);
123
- }
124
- }
125
- }
126
134
  function loadBundled(templateDir, targetDir) {
127
- const src = path.join(BUNDLED_TEMPLATES_DIR, templateDir);
128
- if (!fs.existsSync(src)) {
135
+ const src = path2.join(BUNDLED_TEMPLATES_DIR, templateDir);
136
+ if (!fs2.existsSync(src)) {
129
137
  throw new Error(`Bundled template missing on disk: ${src}`);
130
138
  }
131
139
  const collected = [];
@@ -146,12 +154,12 @@ async function downloadTarball(url, destFile) {
146
154
  });
147
155
  }
148
156
  async function loadRemote(pkgName, targetDir) {
149
- const tmp = await mkdtemp(path.join(os.tmpdir(), "create-objectstack-"));
157
+ const tmp = await mkdtemp(path2.join(os.tmpdir(), "create-objectstack-"));
150
158
  try {
151
- const tarball = path.join(tmp, "templates.tar.gz");
159
+ const tarball = path2.join(tmp, "templates.tar.gz");
152
160
  printStep(`Fetching template "${pkgName}" from ${REMOTE_REPO}@${REMOTE_BRANCH}\u2026`);
153
161
  await downloadTarball(REMOTE_TARBALL_URL, tarball);
154
- fs.mkdirSync(targetDir, { recursive: true });
162
+ fs2.mkdirSync(targetDir, { recursive: true });
155
163
  const collected = [];
156
164
  await pipeline(
157
165
  createReadStream(tarball),
@@ -183,57 +191,57 @@ async function loadRemote(pkgName, targetDir) {
183
191
  }
184
192
  }
185
193
  function walkAndRewriteTs(dir, fn) {
186
- if (!fs.existsSync(dir)) return;
187
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
188
- const full = path.join(dir, entry.name);
194
+ if (!fs2.existsSync(dir)) return;
195
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
196
+ const full = path2.join(dir, entry.name);
189
197
  if (entry.isDirectory()) {
190
198
  walkAndRewriteTs(full, fn);
191
199
  } else if (entry.isFile() && entry.name.endsWith(".ts")) {
192
- const before = fs.readFileSync(full, "utf8");
200
+ const before = fs2.readFileSync(full, "utf8");
193
201
  const after = fn(before);
194
- if (after !== before) fs.writeFileSync(full, after);
202
+ if (after !== before) fs2.writeFileSync(full, after);
195
203
  }
196
204
  }
197
205
  }
198
206
  function rewriteProjectIdentity(targetDir, projectName, namespace) {
199
207
  const title = toTitleCase(projectName);
200
208
  let templateNamespace;
201
- const manifestPathPre = path.join(targetDir, "objectstack.manifest.json");
202
- if (fs.existsSync(manifestPathPre)) {
209
+ const manifestPathPre = path2.join(targetDir, "objectstack.manifest.json");
210
+ if (fs2.existsSync(manifestPathPre)) {
203
211
  try {
204
- const m = JSON.parse(fs.readFileSync(manifestPathPre, "utf8"));
212
+ const m = JSON.parse(fs2.readFileSync(manifestPathPre, "utf8"));
205
213
  if (typeof m.namespace === "string") templateNamespace = m.namespace;
206
214
  } catch {
207
215
  }
208
216
  }
209
- const pkgPath = path.join(targetDir, "package.json");
210
- if (fs.existsSync(pkgPath)) {
217
+ const pkgPath = path2.join(targetDir, "package.json");
218
+ if (fs2.existsSync(pkgPath)) {
211
219
  try {
212
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
220
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
213
221
  pkg.name = projectName;
214
222
  syncObjectStackDeps(pkg, readCliVersion());
215
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
223
+ fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
216
224
  } catch {
217
225
  }
218
226
  }
219
- const manifestPath = path.join(targetDir, "objectstack.manifest.json");
220
- if (fs.existsSync(manifestPath)) {
227
+ const manifestPath = path2.join(targetDir, "objectstack.manifest.json");
228
+ if (fs2.existsSync(manifestPath)) {
221
229
  try {
222
- const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
230
+ const m = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
223
231
  m.name = projectName;
224
232
  m.displayName = title;
225
233
  if ("namespace" in m) m.namespace = namespace;
226
- fs.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
234
+ fs2.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
227
235
  } catch {
228
236
  }
229
237
  }
230
- const configPath = path.join(targetDir, "objectstack.config.ts");
231
- if (fs.existsSync(configPath)) {
232
- let cfg = fs.readFileSync(configPath, "utf8");
238
+ const configPath = path2.join(targetDir, "objectstack.config.ts");
239
+ if (fs2.existsSync(configPath)) {
240
+ let cfg = fs2.readFileSync(configPath, "utf8");
233
241
  cfg = cfg.replace(/(\bid:\s*)(['"`])[^'"`]*\2/, `$1$2${projectName}$2`);
234
242
  cfg = cfg.replace(/(\bnamespace:\s*)(['"`])[^'"`]*\2/, `$1$2${namespace}$2`);
235
243
  cfg = cfg.replace(/(\bname:\s*)(['"`])[^'"`]*\2/, `$1$2${title}$2`);
236
- fs.writeFileSync(configPath, cfg);
244
+ fs2.writeFileSync(configPath, cfg);
237
245
  }
238
246
  if (namespace !== templateNamespace && templateNamespace) {
239
247
  const prefixRe = new RegExp(
@@ -241,39 +249,39 @@ function rewriteProjectIdentity(targetDir, projectName, namespace) {
241
249
  "g"
242
250
  );
243
251
  walkAndRewriteTs(
244
- path.join(targetDir, "src"),
252
+ path2.join(targetDir, "src"),
245
253
  (src) => src.replace(
246
254
  prefixRe,
247
255
  (_m, prefix, q, rest) => `${prefix}${q}${namespace}_${rest}${q}`
248
256
  )
249
257
  );
250
258
  }
251
- const readmePath = path.join(targetDir, "README.md");
252
- if (fs.existsSync(readmePath)) {
253
- let md = fs.readFileSync(readmePath, "utf8");
259
+ const readmePath = path2.join(targetDir, "README.md");
260
+ if (fs2.existsSync(readmePath)) {
261
+ let md = fs2.readFileSync(readmePath, "utf8");
254
262
  md = md.replace(/^#\s+.*$/m, `# ${title}`);
255
- fs.writeFileSync(readmePath, md);
263
+ fs2.writeFileSync(readmePath, md);
256
264
  }
257
265
  writeAgentGuides(targetDir, title, projectName);
258
266
  }
259
267
  function writeAgentGuides(targetDir, title, projectName) {
260
- const templatePath = path.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
268
+ const templatePath = path2.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
261
269
  let template;
262
270
  try {
263
- template = fs.readFileSync(templatePath, "utf8");
271
+ template = fs2.readFileSync(templatePath, "utf8");
264
272
  } catch (err) {
265
273
  if (err?.code === "ENOENT") return;
266
274
  throw err;
267
275
  }
268
276
  const rendered = template.replace(/\{\{PROJECT_TITLE\}\}/g, title).replace(/\{\{PROJECT_NAME\}\}/g, projectName);
269
- writeIfAbsent(path.join(targetDir, "AGENTS.md"), rendered);
270
- const copilotPath = path.join(targetDir, ".github", "copilot-instructions.md");
271
- fs.mkdirSync(path.dirname(copilotPath), { recursive: true });
277
+ writeIfAbsent(path2.join(targetDir, "AGENTS.md"), rendered);
278
+ const copilotPath = path2.join(targetDir, ".github", "copilot-instructions.md");
279
+ fs2.mkdirSync(path2.dirname(copilotPath), { recursive: true });
272
280
  writeIfAbsent(copilotPath, rendered);
273
281
  }
274
282
  function writeIfAbsent(filePath, contents) {
275
283
  try {
276
- fs.writeFileSync(filePath, contents, { flag: "wx" });
284
+ fs2.writeFileSync(filePath, contents, { flag: "wx" });
277
285
  } catch (err) {
278
286
  if (err?.code !== "EEXIST") throw err;
279
287
  }
@@ -295,24 +303,24 @@ var program = new Command().name("create-objectstack").description("Create a new
295
303
  process.exit(1);
296
304
  }
297
305
  const cwd = process.cwd();
298
- const projectName = name || path.basename(cwd);
306
+ const projectName = name || path2.basename(cwd);
299
307
  const namespace = sanitizeNamespace(projectName);
300
- const targetDir = name ? path.resolve(cwd, name) : cwd;
308
+ const targetDir = name ? path2.resolve(cwd, name) : cwd;
301
309
  const isCurrentDir = targetDir === cwd;
302
310
  printKV("Environment", projectName);
303
311
  printKV("Namespace", namespace);
304
312
  printKV("Template", `${options.template} \u2014 ${template.description}`);
305
313
  printKV("Directory", targetDir);
306
314
  console.log("");
307
- if (!isCurrentDir && fs.existsSync(targetDir)) {
308
- const existing = fs.readdirSync(targetDir);
315
+ if (!isCurrentDir && fs2.existsSync(targetDir)) {
316
+ const existing = fs2.readdirSync(targetDir);
309
317
  if (existing.length > 0) {
310
318
  printError(`Directory already exists and is not empty: ${targetDir}`);
311
319
  process.exit(1);
312
320
  }
313
321
  }
314
322
  try {
315
- fs.mkdirSync(targetDir, { recursive: true });
323
+ fs2.mkdirSync(targetDir, { recursive: true });
316
324
  let createdFiles;
317
325
  if (template.source.kind === "bundled") {
318
326
  createdFiles = loadBundled(template.source.dir, targetDir);
@@ -342,14 +350,14 @@ var program = new Command().name("create-objectstack").description("Create a new
342
350
  if (!options.skipInstall && !options.skipSkills) {
343
351
  printStep("Installing AI skills for your coding agent...");
344
352
  try {
345
- execSync("npx -y skills add objectstack-ai/framework --all", {
353
+ execSync("npx -y skills add objectstack-ai/framework/skills --all", {
346
354
  stdio: "inherit",
347
355
  cwd: targetDir
348
356
  });
349
357
  console.log("");
350
358
  } catch {
351
359
  printWarning(
352
- "Skills installation skipped. Run manually:\n npx skills add objectstack-ai/framework"
360
+ "Skills installation skipped. Run manually:\n npx skills add objectstack-ai/framework/skills"
353
361
  );
354
362
  console.log("");
355
363
  }
@@ -369,7 +377,7 @@ var program = new Command().name("create-objectstack").description("Create a new
369
377
  if (options.skipInstall || options.skipSkills) {
370
378
  console.log("");
371
379
  console.log(chalk.bold(" AI Skills (recommended):"));
372
- console.log(chalk.dim(" npx skills add objectstack-ai/framework"));
380
+ console.log(chalk.dim(" npx skills add objectstack-ai/framework/skills"));
373
381
  }
374
382
  console.log("");
375
383
  } catch (error) {
@@ -77,7 +77,7 @@ This project uses ObjectStack skills from `objectstack-ai/framework`.
77
77
  Install or update skills with the standard [skills CLI](https://skills.sh/):
78
78
 
79
79
  ```bash
80
- npx skills add objectstack-ai/framework
80
+ npx skills add objectstack-ai/framework/skills
81
81
  ```
82
82
 
83
83
  Skills are triggered automatically based on task context:
@@ -21,11 +21,55 @@ curl -c cookies.txt -X POST http://localhost:3000/api/v1/auth/sign-in/email \
21
21
  curl -b cookies.txt "http://localhost:3000/api/v1/data/<your_object>"
22
22
  ```
23
23
 
24
+ ## Your app is an MCP server
25
+
26
+ Every ObjectStack app is itself a
27
+ [Model Context Protocol](https://modelcontextprotocol.io) server — **on by
28
+ default**, no plugin to install. `pnpm dev` prints the endpoint and a
29
+ ready-to-paste connect command on boot; point a coding agent (Claude Code,
30
+ Cursor, any MCP client) at it and it can read your schema, query data, and run
31
+ your exposed actions — all under the caller's own permissions and RLS:
32
+
33
+ ```bash
34
+ claude mcp add --transport http my-app http://localhost:3000/api/v1/mcp
35
+ ```
36
+
37
+ Set `OS_MCP_SERVER_ENABLED=false` to turn it off. This is the *serve* side — the
38
+ reverse of the `mcp` connector below (which lets your app *call* other MCP
39
+ servers). See [Connect an MCP Client](https://docs.objectstack.ai/docs/ai/connect-mcp)
40
+ for OAuth, API keys, and which objects/actions become tools.
41
+
24
42
  ## Layout
25
43
 
26
44
  - `objectstack.config.ts` — environment manifest (objects, API, plugins)
27
45
  - `src/objects/` — object definitions (one file per object)
28
46
 
47
+ ## Connectors (default providers)
48
+
49
+ `objectstack.config.ts` wires the three **generic connector executors**, so you
50
+ can call an external system from a flow as pure metadata — no host code:
51
+
52
+ | Provider | Package | Use for |
53
+ |:---|:---|:---|
54
+ | `rest` | `@objectstack/connector-rest` | Any JSON/HTTP REST API |
55
+ | `openapi` | `@objectstack/connector-openapi` | An API described by an OpenAPI document |
56
+ | `mcp` | `@objectstack/connector-mcp` | A Model Context Protocol server |
57
+
58
+ Add a `connectors:` entry that names one of these `provider`s and the
59
+ `automation` capability materializes it into a live, dispatchable connector at
60
+ boot (ADR-0097); a flow's `connector_action` node then calls it. To add a brand
61
+ connector (e.g. Slack), install its package and add `new ConnectorSlackPlugin()`
62
+ to `plugins:`; to drop a provider, remove its plugin.
63
+
64
+ > **Security — declarative MCP over stdio.** An `mcp` connector whose transport
65
+ > spawns a local process (`stdio`) is denied by default, because the command
66
+ > comes from metadata. Opt in per host with
67
+ > `new ConnectorMcpPlugin({ declarativeStdio: ['node'] })`; `http` transports
68
+ > need no opt-in.
69
+
70
+ See [Automation → Flows](https://docs.objectstack.ai/docs/automation/flows) for
71
+ the full connector and `connector_action` guide.
72
+
29
73
  ## Verify your changes
30
74
 
31
75
  After editing any metadata, run:
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ .objectstack/
4
+ *.log
5
+ .DS_Store
6
+ .env
7
+ .env.*
@@ -1,4 +1,7 @@
1
1
  import { defineStack } from '@objectstack/spec';
2
+ import { ConnectorRestPlugin } from '@objectstack/connector-rest';
3
+ import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
4
+ import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
2
5
  import * as objects from './src/objects/index.js';
3
6
 
4
7
  export default defineStack({
@@ -13,7 +16,28 @@ export default defineStack({
13
16
  // refuse this package at the boundary with the exact migration command,
14
17
  // instead of crashing later. Kept in lockstep with releases by
15
18
  // scripts/sync-template-versions.mjs.
16
- engines: { protocol: '^15' },
19
+ engines: { protocol: '^16' },
17
20
  },
21
+
22
+ // `automation` backs flow execution and, per ADR-0097, materializes any
23
+ // declarative `connectors:` entry into a live, dispatchable connector at boot.
24
+ // The connector executors below register their provider factories with it —
25
+ // without `automation` loaded they have nowhere to register and boot fails, so
26
+ // keep this capability whenever `plugins:` lists a connector.
27
+ requires: ['automation'],
28
+
29
+ // Generic connector executors (ADR-0022/0023/0024 + ADR-0097), default-present
30
+ // so you can add a `connectors:` entry naming `provider: 'rest' | 'openapi' |
31
+ // 'mcp'` and have it materialize with zero host code. Zero-arg = contribute the
32
+ // provider factory only. Brand connectors (Slack, …) stay marketplace/opt-in.
33
+ // Security (#3055): a declarative `mcp` stdio transport spawns a local process
34
+ // from metadata and is denied by default — opt in per host with
35
+ // `new ConnectorMcpPlugin({ declarativeStdio: ['<trusted-command>'] })`.
36
+ plugins: [
37
+ new ConnectorRestPlugin(),
38
+ new ConnectorOpenApiPlugin(),
39
+ new ConnectorMcpPlugin(),
40
+ ],
41
+
18
42
  objects: Object.values(objects),
19
43
  });
@@ -11,13 +11,16 @@
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@objectstack/spec": "^15.0.0",
15
- "@objectstack/runtime": "^15.0.0",
16
- "@objectstack/driver-memory": "^15.0.0",
17
- "@objectstack/plugin-hono-server": "^15.0.0"
14
+ "@objectstack/spec": "^16.0.0",
15
+ "@objectstack/runtime": "^16.0.0",
16
+ "@objectstack/driver-memory": "^16.0.0",
17
+ "@objectstack/plugin-hono-server": "^16.0.0",
18
+ "@objectstack/connector-rest": "^16.0.0",
19
+ "@objectstack/connector-openapi": "^16.0.0",
20
+ "@objectstack/connector-mcp": "^16.0.0"
18
21
  },
19
22
  "devDependencies": {
20
- "@objectstack/cli": "^15.0.0",
23
+ "@objectstack/cli": "^16.0.0",
21
24
  "typescript": "^6.0.0"
22
25
  }
23
26
  }
@@ -0,0 +1,23 @@
1
+ # pnpm does not run dependency install scripts unless they are approved here.
2
+ # Without this file a fresh `pnpm install` on pnpm 11 exits 1 with
3
+ # ERR_PNPM_IGNORED_BUILDS — pnpm 10 only warned, pnpm 11 made it a hard error.
4
+ #
5
+ # Both keys are needed; they are read by different pnpm versions:
6
+ # allowBuilds pnpm >= 10.31 and pnpm 11+. pnpm 11 reads ONLY this
7
+ # one — onlyBuiltDependencies alone still errors.
8
+ # onlyBuiltDependencies pnpm 10.0–10.30, which do not understand allowBuilds.
9
+ #
10
+ # better-sqlite3 is the native sqlite driver (@objectstack/driver-sql's optional
11
+ # dependency); esbuild compiles objectstack.config.ts. Both ship prebuilt
12
+ # binaries, so an unapproved build degrades rather than breaks — but pnpm 11
13
+ # fails the install before it gets that far.
14
+ #
15
+ # npm and yarn ignore this file; it costs them nothing.
16
+
17
+ onlyBuiltDependencies:
18
+ - better-sqlite3
19
+ - esbuild
20
+
21
+ allowBuilds:
22
+ better-sqlite3: true
23
+ esbuild: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-objectstack",
3
- "version": "15.1.1",
3
+ "version": "16.0.0-rc.0",
4
4
  "description": "Create a new ObjectStack project — npx create-objectstack",
5
5
  "bin": {
6
6
  "create-objectstack": "./bin/create-objectstack.js"