Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ that an LLM can read, diff, and safely modify without breaking the runtime.
| [`packages/expense/`](./packages/expense) | Employee expense & reimbursement — multi-line reports, category policy, amount-tiered approval, reimbursement tracking | ✅ v0 | 4011 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/objectstack-ai/templates/tree/main/packages/expense) |
| `packages/sales-pipeline/` | Lite CRM (leads, opportunities, accounts) | 🚧 planned | 4007 | — |

### Run them all at once

Want every template in **one runtime, one app launcher** — as if you installed
them all together from the marketplace? Use the `all` aggregator:

```bash
pnpm dev:all # builds every template, composes them, boots on :4000
```

It compiles each template's artifact into a single **environment** bundle and
serves it (admin: `admin@objectos.ai` / `admin123`). See
[`packages/all/`](./packages/all) for how composition happens at the
environment layer without breaking the one-app-per-package rule (ADR-0019).

> **StackBlitz tip:** every template runs in the browser via `@objectstack/driver-sqlite-wasm` (sql.js). Local dev still uses `better-sqlite3` (listed as an optional dependency) for full native speed.

> Looking for a full reference app? See [hotcrm](https://github.com/objectstack-ai/hotcrm) — the production-grade CRM built on the same engine.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"scripts": {
"dev": "pnpm --filter @objectlab/todo dev",
"dev:all": "pnpm -r --filter=!@objectlab/all build && pnpm --filter @objectlab/all dev",
"typecheck": "pnpm -r typecheck",
"build": "pnpm -r build",
"test": "pnpm -r test",
Expand Down
96 changes: 96 additions & 0 deletions packages/all/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# `@objectlab/all` — install everything from the marketplace

A throwaway **workspace** that runs *every* template in this repo in **one
runtime**, behind **one app launcher** — as if you had installed all of them
together from the marketplace.

It is not a template. It ships no objects of its own. It is an
**environment compiler**: it gathers the compiled artifact of each template and
composes them into a single environment bundle the runtime serves verbatim.

```bash
# from the repo root — build every template first
pnpm -r build

# then, from this package
pnpm --filter @objectlab/all dev # compile + boot all apps on :4000
```

Open <http://localhost:4000/_console/> and you'll find all nine apps
(`todo`, `compliance`, `content`, `contracts`, `expense`, `helpdesk`, `hr`,
`procurement`, `pm`) side-by-side, each with its own namespace and seed data.

Seeded admin: `admin@objectos.ai` / `admin123`.

## How it works

This piggy-backs on the runtime's **real** local-install mechanism. When you
install an App from the marketplace, `MarketplaceInstallLocalPlugin` writes it to
`.objectstack/installed-packages/<manifestId>.json` (a wrapper
`{ packageId, manifestId, version, manifest, … }` whose `manifest` is the App's
full compiled artifact) and rehydrates it on boot. The compile command reads
that same folder and composes everything into one static environment artifact.

```
packages/*/dist/objectstack.json ← each template, compiled & self-contained
│ (1) install → wrapper entry, runtime's exact format
.objectstack/installed-packages/<id>.json ← the REAL local-install folder
│ (2) compile (scripts/compile-marketplace.mjs)
dist/objectstack.json ← ONE environment artifact: apps[9]
│ (3) serve
objectstack dev all --artifact dist/objectstack.json
```

1. **install** — populate `.objectstack/installed-packages/` from the workspace
templates, in the runtime's exact wrapper format, so the folder is
indistinguishable from a real marketplace install. Already-present entries are
left untouched — if you installed an App via the marketplace UI (or dropped a
compiled artifact into the folder), it's picked up as-is.
2. **compile** — unwrap each installed entry to its artifact, concatenate all
metadata (objects, apps, views, flows, hooks, data…), de-duplicate
environment-level singletons (`roles` / `permissions` by name, `requires` as a
set), and synthesize one environment manifest with `apps[N]`.
3. **serve** — `objectstack dev all --artifact …` loads the JSON directly and
boots a single runtime hosting every app.

Scripts:

| Command | Does |
|---|---|
| `pnpm compile` | (re)build `dist/objectstack.json` from the installed store |
| `pnpm dev` | compile, then boot on `:4000` with a fresh ephemeral DB + seeded admin |
| `pnpm start` | boot the already-compiled artifact (no recompile, persistent DB) |

## Why this does not break ADR-0019 ("one app per package")

ADR-0019 forbids an **authored package** (`type: 'app'`) from defining more than
one app — the banned "suite contains apps" shape — and `defineStack()` enforces
it via `validateSingleApp`.

That rule governs **authoring a package**. It does **not** govern the
**environment** a tenant runs: an environment legitimately hosts many
independently-installed apps, each keeping its own namespace (the runtime keys
namespaces as `Map<namespace, Set<packageId>>`). The cloud control plane does
exactly this when it compiles a tenant environment from its installed packages.

So we compose at the **environment layer** and emit the merged artifact
directly. We never wrap the result in `defineStack()`; the CLI's `compile`/serve
path validates with `ObjectStackDefinitionSchema` (schema only), not the
`defineStack` wrapper — so the single-app and namespace-prefix gates correctly
do not apply. This is the same reason `composeStacks()` exists in
`@objectstack/spec`: composition is an environment-assembly primitive, not a
package-authoring one.

## Caveats

- **Generic role/permission names collide.** `todo` and `content` both ship a
`lead` / `contributor` role and permission; the compiler keeps the first and
shadows the rest (logged on compile). Fine for a "see everything" dev
environment; for production isolation each app would namespace its roles.
- **Throwaway, not a product.** This package is for local exploration, demos,
and cross-template QA — not something you publish to the marketplace.
- Rebuild a template (`pnpm --filter @objectlab/<name> build`) then
`pnpm --filter @objectlab/all compile` to pick up its changes.
27 changes: 27 additions & 0 deletions packages/all/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@objectlab/all",
"version": "0.1.0",
"description": "Install-everything workspace — composes every ObjectStack template into one runtime, as if installed together from the marketplace.",
"license": "Apache-2.0",
"private": true,
"type": "module",
"scripts": {
"compile": "node scripts/compile-marketplace.mjs",
"dev": "pnpm run compile && objectstack dev all --artifact dist/objectstack.json -p 4000 --fresh",
"start": "objectstack dev all --artifact dist/objectstack.json -p 4000"
},
"dependencies": {
"@objectstack/account": "^7.4.1",
"@objectstack/cli": "^7.4.1",
"@objectstack/driver-sqlite-wasm": "^7.4.1",
"@objectstack/runtime": "^7.4.1",
"sql.js": "^1.14.1"
},
"optionalDependencies": {
"better-sqlite3": "^12.10.0"
},
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@10.33.0"
}
224 changes: 224 additions & 0 deletions packages/all/scripts/compile-marketplace.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#!/usr/bin/env node
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
//
// Marketplace environment compiler — compile whatever is installed locally.
//
// When you install Apps from the marketplace, the runtime's
// `MarketplaceInstallLocalPlugin` persists each one to disk:
//
// <cwd>/.objectstack/installed-packages/<manifestId>.json
//
// …as a wrapper entry `{ packageId, manifestId, version, manifest, … }` where
// `manifest` is the App's full compiled artifact. The runtime rehydrates these
// at boot. This command does the offline equivalent: it reads that same folder
// and composes every installed App into ONE environment artifact
// (`dist/objectstack.json`) that `objectstack dev all --artifact …` serves as a
// single runtime hosting every app — "install everything from the marketplace".
//
// Two verbs, both idempotent:
// • install — populate `.objectstack/installed-packages/` from the workspace
// templates, in the EXACT wrapper format the runtime writes, so
// the folder is indistinguishable from a real local install.
// (Skipped automatically if the folder is already populated —
// e.g. you installed via the marketplace UI.)
// • compile — read every installed entry and compose → dist/objectstack.json
//
// WHY THIS IS NOT A PROTOCOL VIOLATION (ADR-0019)
// -----------------------------------------------
// ADR-0019 bans an *authored package* (`type:'app'`) from defining more than one
// app (the "suite contains apps" shape), enforced by `defineStack`'s
// `validateSingleApp`. That rule governs *authoring a package*. It does NOT
// govern the *environment* a tenant runs: an environment legitimately hosts many
// independently-installed Apps, each keeping its own namespace (the runtime keys
// namespaces as `Map<namespace, Set<packageId>>`) — which is exactly what the
// rehydrate-at-boot path above already does. We compose at the ENVIRONMENT layer
// and emit the merged artifact directly; we never wrap it in `defineStack()`,
// and the CLI's serve path validates with `ObjectStackDefinitionSchema` (schema
// only), so the single-app / namespace-prefix gates correctly do not apply.

import {
readFileSync,
writeFileSync,
mkdirSync,
readdirSync,
existsSync,
} from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const ALL_DIR = resolve(HERE, '..'); // packages/all (the runtime cwd for `dev all`)
const PACKAGES_DIR = resolve(ALL_DIR, '..'); // packages

// The real on-disk location the runtime's MarketplaceInstallLocalPlugin uses:
// storageDir = resolve(process.cwd(), '.objectstack/installed-packages')
const INSTALLED_DIR = join(ALL_DIR, '.objectstack', 'installed-packages');
const OUT = join(ALL_DIR, 'dist', 'objectstack.json');

// Mirror MarketplaceInstallLocalPlugin.safeFilename so files we write are
// byte-identical to a real local install.
const safeFilename = (manifestId) => `${manifestId.replace(/[^a-zA-Z0-9._-]/g, '_')}.json`;

// Arrays concatenated at the environment layer (mirrors `composeStacks`'
// CONCAT_ARRAY_FIELDS, minus the singletons we de-dupe below).
const CONCAT_FIELDS = [
'translations', 'objectExtensions', 'objects', 'apps', 'views', 'pages',
'dashboards', 'reports', 'actions', 'themes', 'flows', 'jobs',
'emailTemplates', 'sharingRules', 'policies', 'apis', 'webhooks', 'agents',
'skills', 'hooks', 'mappings', 'analyticsCubes', 'connectors', 'datasources',
'portals', 'data',
];

// Environment-level singletons keyed by `name`. Two installed apps can each ship
// a generic "lead"/"contributor" role/permission — keep the first, shadow the
// rest, so the runtime never double-registers.
const DEDUP_BY_NAME = ['roles', 'permissions'];

const log = (msg) => process.stdout.write(`${msg}\n`);

/**
* `install` — populate `.objectstack/installed-packages/` from the workspace
* templates, in the runtime's wrapper format. No-op for templates already
* present (so a real marketplace install is never clobbered).
*/
function installFromWorkspace() {
mkdirSync(INSTALLED_DIR, { recursive: true });
const installed = [];
for (const name of readdirSync(PACKAGES_DIR).sort()) {
if (name === 'all') continue;
const artifactPath = join(PACKAGES_DIR, name, 'dist', 'objectstack.json');
if (!existsSync(artifactPath)) {
log(` · skip ${name} (no dist/objectstack.json — run \`pnpm -r build\` first)`);
continue;
}
const artifact = JSON.parse(readFileSync(artifactPath, 'utf8'));
const manifestId = artifact?.manifest?.id ?? `app.objectstack.template.${name}`;
const dest = join(INSTALLED_DIR, safeFilename(manifestId));
if (existsSync(dest)) {
installed.push(`${name} (already installed)`);
continue;
}
const entry = {
packageId: manifestId,
versionId: artifact?.manifest?.version ?? '0.0.0',
manifestId,
version: artifact?.manifest?.version ?? '0.0.0',
manifest: artifact, // the App's full compiled artifact
installedAt: '1970-01-01T00:00:00.000Z', // fixed → deterministic compile
installedBy: 'compile-marketplace',
withSampleData: false,
};
writeFileSync(dest, `${JSON.stringify(entry, null, 2)}\n`, 'utf8');
installed.push(name);
}
return installed;
}

/** Read every installed entry; unwrap to the App's full artifact. */
function readInstalledArtifacts() {
if (!existsSync(INSTALLED_DIR)) return [];
const out = [];
for (const file of readdirSync(INSTALLED_DIR).filter((f) => f.endsWith('.json')).sort()) {
let entry;
try {
entry = JSON.parse(readFileSync(join(INSTALLED_DIR, file), 'utf8'));
} catch {
log(` ! ${file}: invalid JSON, skipped`);
continue;
}
// Accept either the runtime wrapper ({ manifest: <artifact> }) or a bare
// artifact dropped straight into the folder.
const artifact = entry?.manifest?.objects || entry?.manifest?.apps ? entry.manifest : entry;
const source = artifact?.manifest?.namespace || entry?.manifestId || file.replace(/\.json$/, '');
out.push({ source, artifact });
}
return out;
}

/** `compile` — compose installed Apps into one environment artifact. */
function compile(store) {
const env = {
manifest: {
id: 'app.objectstack.environment.all',
name: 'All Templates',
version: '0.1.0',
type: 'app',
description:
'Local environment with every installed App composed together — the "install everything from the marketplace" workspace.',
},
};

const requires = new Set();
const supportedLocales = new Set();
let defaultLocale = 'en';
const dedup = Object.fromEntries(DEDUP_BY_NAME.map((k) => [k, new Map()]));
const objectOwner = new Map();

for (const { artifact, source } of store) {
for (const obj of artifact.objects ?? []) {
if (objectOwner.has(obj.name)) {
throw new Error(
`object '${obj.name}' is defined by both '${objectOwner.get(obj.name)}' and '${source}'. ` +
`Apps must prefix every object with their namespace (see TEMPLATE_GUIDE.md).`,
);
}
objectOwner.set(obj.name, source);
}

for (const field of CONCAT_FIELDS) {
const value = artifact[field];
if (Array.isArray(value) && value.length > 0) (env[field] ??= []).push(...value);
}

for (const field of DEDUP_BY_NAME) {
for (const item of artifact[field] ?? []) {
if (!dedup[field].has(item.name)) dedup[field].set(item.name, item);
else log(` ! ${field.slice(0, -1)} '${item.name}' from '${source}' shadowed (already installed)`);
}
}

for (const token of artifact.requires ?? []) requires.add(token);

if (artifact.i18n) {
if (artifact.i18n.defaultLocale) defaultLocale = artifact.i18n.defaultLocale;
for (const locale of artifact.i18n.supportedLocales ?? []) supportedLocales.add(locale);
}
}

for (const field of DEDUP_BY_NAME) {
const items = [...dedup[field].values()];
if (items.length > 0) env[field] = items;
}
if (requires.size > 0) env.requires = [...requires];

supportedLocales.add(defaultLocale);
env.i18n = { defaultLocale, supportedLocales: [...supportedLocales], fallbackLocale: defaultLocale };

return env;
}

// ── main ────────────────────────────────────────────────────────────
log('▶ Compiling marketplace environment (install-all)\n');

const justInstalled = installFromWorkspace();
if (justInstalled.length > 0) log(` installed-packages: ${justInstalled.join(', ')}`);

const store = readInstalledArtifacts();
if (store.length === 0) {
log('\n✗ No installed packages found in .objectstack/installed-packages/.');
log(' Build the templates first: pnpm -r build');
log(' Or install Apps via the marketplace, then re-run this command.');
process.exit(1);
}

const env = compile(store);
mkdirSync(dirname(OUT), { recursive: true });
writeFileSync(OUT, `${JSON.stringify(env, null, 2)}\n`);

log('');
log(`✓ Composed ${store.length} apps · ${env.objects?.length ?? 0} objects · ${env.flows?.length ?? 0} flows`);
log(` apps: ${(env.apps ?? []).map((a) => a.name).join(', ')}`);
log(` installed-packages: ${INSTALLED_DIR.replace(`${PACKAGES_DIR}/`, '')}`);
log(` → ${OUT.replace(`${PACKAGES_DIR}/`, '')}`);
log('');
log('Run it: objectstack dev all --artifact dist/objectstack.json -p 4000 --fresh');
Loading
Loading