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
23 changes: 23 additions & 0 deletions .changeset/retire-remote-template-catalog.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
---
'create-objectstack': minor
---

Retire the five remote content templates from the scaffolder's catalog.

`todo`, `compliance`, `content`, `contracts` and `procurement` were delisted
from the official ObjectStack template marketplace and are no longer
maintained, but the CLI carried its own hardcoded catalog and never learned
that: `--help` recommended all five by name with marketing descriptions, and
the `Available:` line on a bad `-t` offered them too.

- `blank` (bundled, offline) is now the whole catalog, so the help text
advertises only what is actually supported.
- Asking for one of the five by name — `-t todo` in an old script or tutorial —
is refused with a message that says the template was retired, instead of the
generic "Unknown template" error that reads as a typo.
- The GitHub tarball-fetch path that served the remote templates is removed
along with its `tar` dependency; nothing else reached it.

Note this corrects the catalog at HEAD only. Already-published versions keep
advertising the retired templates until a new version of `create-objectstack`
is released.
10 changes: 8 additions & 2 deletions .github/workflows/scaffold-e2e.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,7 +252,11 @@ jobs:
strategy:
fail-fast: false
matrix:
template: [blank, todo, compliance, content, contracts, procurement]
# `blank` is the whole catalog: the five remote content templates
# (todo, compliance, content, contracts, procurement) were delisted
# from the marketplace and retired from the scaffolder, so canarying
# them would gate the release on unmaintained content.
template: [blank]
steps:
- name: Setup Node.js
uses: actions/setup-node@v7
Expand All@@ -264,7 +268,9 @@ jobs:
cd "$RUNNER_TEMP"
npx -y create-objectstack@latest canary-app -t ${{ matrix.template }} --skip-skills

# Remote templates ship `build` (same gates) but not always `validate`.
# `build` is the gate every template must pass; `validate` is run only
# when the scaffolded project defines it (the now-retired remote
# templates did not always ship it).
- name: Gate the generated project
run: |
cd "$RUNNER_TEMP/canary-app"
Expand Down
17 changes: 7 additions & 10 deletions content/docs/getting-started/your-first-project.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,20 +58,17 @@ bundle) with `--skip-skills` if you prefer — `AGENTS.md` is always written.
| Template | Source | Description |
|:---|:---|:---|
| `blank` *(default)* | bundled, works offline | One object, REST API — a clean slate |
| `todo` | remote | Universal task & project management starter |
| `compliance` | remote | Compliance posture & evidence management (SOC2 / ISO27001) |
| `content` | remote | Content marketing pipeline — editorial calendar & channel ROI |
| `contracts` | remote | Post-signature CLM — approvals, obligations, renewals |
| `procurement` | remote | Source-to-pay — vendors, POs, receipts, invoice matching |

```bash
npm create objectstack@latest my-app -- --template todo
npm create objectstack@latest my-app -- --template blank
```

Remote templates are fetched from the
[`objectstack-ai/templates`](https://github.com/objectstack-ai/templates)
repository at scaffold time and need network access; `blank` is bundled inside
the npm package and always works offline.
`blank` is bundled inside the npm package, so scaffolding always works offline.

The remote content templates (`todo`, `compliance`, `content`, `contracts`,
`procurement`) have been retired: they were delisted from the ObjectStack
template marketplace and are no longer maintained. Asking for one by name says
so explicitly.

<Callout type="tip">
`os init` (from `@objectstack/cli`) is an alternative scaffolder with `app` /
Expand Down
2 changes: 1 addition & 1 deletion content/docs/plugins/packages.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -434,7 +434,7 @@ npx os serve --dev

**Project Scaffolding** — Create new ObjectStack projects.

- **Templates**: `blank` (default, bundled), plus remote templates `todo`, `compliance`, `content`, `contracts`, `procurement`
- **Templates**: `blank` (default, bundled, works offline) — the remote content templates (`todo`, `compliance`, `content`, `contracts`, `procurement`) are retired
- **When to use**: Start a new ObjectStack project
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/create-objectstack/README.md)

Expand Down
7 changes: 4 additions & 3 deletions docs/qa/platform-checklist/areas/cli.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -360,7 +360,7 @@
"title": "The published first-run closes: create-objectstack scaffold → install → validate → build → boot → health, with the skills boundary holding",
"since": "v15",
"status": "active",
"revision": 1,
"revision": 2,
"priority": "P1",
"surface": "mixed",
"personas": ["new user (published-registry consumer)"],
Expand DownExpand Up@@ -416,15 +416,16 @@
"an internal skill appearing in a scaffolded project is the exact leak the boundary step exists for — FAIL",
"a template that builds only from the repo checkout but not from the registry is the #2908 class this whole item guards"
],
"variants": ["blank", "todo", "compliance", "content", "contracts", "procurement"],
"variants": ["blank"],
"traps": ["stale-dist"],
"automated": { "kind": "ci", "ref": ".github/workflows/scaffold-e2e.yml" },
"source": [
".github/workflows/scaffold-e2e.yml (#2908 — the scaffold→install→validate→build→boot→health lane, the registry-canary template matrix, the skills-boundary assertions, the #4894 RC-window fallback)",
"packages/create-objectstack (the scaffolder under test)"
],
"history": [
{ "revision": 1, "date": "2026-08-07", "change": "new item: the published first-run experience mirrored step-for-step from scaffold-e2e.yml, template matrix as variants, RC-window protocol refusal recorded as gate-working instead of failure", "ref": "claude/platform-test-checklist-ocwugl" }
{ "revision": 1, "date": "2026-08-07", "change": "new item: the published first-run experience mirrored step-for-step from scaffold-e2e.yml, template matrix as variants, RC-window protocol refusal recorded as gate-working instead of failure", "ref": "claude/platform-test-checklist-ocwugl" },
{ "revision": 2, "date": "2026-08-14", "change": "variants narrowed to [blank]: the five remote content templates were delisted from the marketplace and retired from the create-objectstack catalog, and the scaffold-e2e registry-canary matrix this item mirrors was trimmed with them", "ref": "claude/issue-8677-retire-remote-template-catalog" }
]
},
{
Expand Down
18 changes: 8 additions & 10 deletions packages/create-objectstack/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,16 +27,14 @@ npx create-objectstack my-app --skip-install
| Template | Source | Description |
| --- | --- | --- |
| `blank` *(default)* | bundled (offline) | Minimal starter — one object, REST API, ready to extend |
| `todo` | remote | Universal task & project management starter |
| `compliance` | remote | Compliance posture & evidence management (SOC2 / ISO27001) |
| `content` | remote | Content marketing pipeline — editorial calendar & channel ROI |
| `contracts` | remote | Post-signature CLM — approvals, obligations, renewals |
| `procurement` | remote | Source-to-pay — vendors, POs, receipts, invoice matching |

Remote templates are fetched from
[`objectstack-ai/templates`](https://github.com/objectstack-ai/templates) at
scaffold time and require network access; `blank` is bundled and always works
offline.

`blank` is bundled inside the npm package, so scaffolding never needs network
access.

The remote content templates (`todo`, `compliance`, `content`, `contracts`,
`procurement`) have been **retired** — they were delisted from the ObjectStack
template marketplace and are no longer maintained. Asking for one by name tells
you so rather than failing as an unknown template.

## Options

Expand Down
3 changes: 1 addition & 2 deletions packages/create-objectstack/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,7 @@
"license": "Apache-2.0",
"dependencies": {
"chalk": "^6.0.0",
"commander": "^15.0.0",
"tar": "^7.5.22"
"commander": "^15.0.0"
},
"devDependencies": {
"@types/node": "^26.1.2",
Expand Down
175 changes: 36 additions & 139 deletions packages/create-objectstack/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,19 +3,19 @@
/**
* create-objectstack — scaffold a new ObjectStack environment.
*
* Two template sources:
* One template source: the bundled `blank` template. It lives at
* `dist/templates/blank/` (copied from `src/templates/blank/` by tsup
* `onSuccess`) and is cloned via recursive fs copy, which also restores the
* placeholder names npm strips at publish (see TEMPLATE_FILE_ALIASES). Always
* available offline.
*
* 1. Bundled `blank` template
* Lives at `dist/templates/blank/` (copied from `src/templates/blank/`
* by tsup `onSuccess`). Cloned via recursive fs copy, which also restores
* the placeholder names npm strips at publish (see TEMPLATE_FILE_ALIASES).
* Always available offline.
*
* 2. Remote content templates (`todo`, `compliance`, `content`,
* `contracts`, `procurement`)
* Fetched as a single tarball from the sibling repo
* `objectstack-ai/templates` on GitHub, then the `packages/<name>/`
* subtree is extracted. Requires network.
* There used to be a second category — remote content templates (`todo`,
* `compliance`, `content`, `contracts`, `procurement`) fetched as a tarball
* from the sibling repo `objectstack-ai/templates`. Those five were delisted
* from the official marketplace and are no longer maintained, so the catalog
* no longer offers them and the tarball-fetch path that served them is gone.
* `template-registry.ts` still names them, so `-t todo` refuses with an
* explanation instead of a bare "unknown template".
*
* After the files land in `targetDir`, four files are rewritten with the
* user-supplied project name:
Expand All@@ -35,15 +35,8 @@ import { Command } from 'commander';
import chalk from 'chalk';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { pipeline } from 'node:stream/promises';
import { createGunzip } from 'node:zlib';
import { createWriteStream, createReadStream } from 'node:fs';
import { mkdtemp, rm } from 'node:fs/promises';
// eslint-disable-next-line import/no-unresolved
import * as tar from 'tar';

import { syncObjectStackDeps } from './pkg-utils.js';
import { copyDir } from './template-copy.js';
Expand All@@ -52,53 +45,12 @@ import {
rewriteObjectNamePrefix,
findStaleNamespacePrefixes,
} from './rewrite-identity.js';
import { lookupTemplate, templateNames } from './template-registry.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const BUNDLED_TEMPLATES_DIR = path.resolve(__dirname, 'templates');

const REMOTE_REPO = 'objectstack-ai/templates';
const REMOTE_BRANCH = 'main';
const REMOTE_TARBALL_URL = `https://codeload.github.com/${REMOTE_REPO}/tar.gz/refs/heads/${REMOTE_BRANCH}`;

// ─── Template Registry ──────────────────────────────────────────────

type TemplateSource =
| { kind: 'bundled'; dir: string }
| { kind: 'remote'; pkg: string };

interface TemplateInfo {
description: string;
source: TemplateSource;
}

const TEMPLATES: Record<string, TemplateInfo> = {
blank: {
description: 'Minimal starter — one object, REST API, ready to extend',
source: { kind: 'bundled', dir: 'blank' },
},
todo: {
description: 'Universal task & project management starter',
source: { kind: 'remote', pkg: 'todo' },
},
compliance: {
description: 'Compliance posture & evidence management (SOC2 / ISO27001)',
source: { kind: 'remote', pkg: 'compliance' },
},
content: {
description: 'Content marketing pipeline — editorial calendar & channel ROI',
source: { kind: 'remote', pkg: 'content' },
},
contracts: {
description: 'Post-signature CLM — approvals, obligations, renewals',
source: { kind: 'remote', pkg: 'contracts' },
},
procurement: {
description: 'Source-to-pay — vendors, POs, receipts, invoice matching',
source: { kind: 'remote', pkg: 'procurement' },
},
};

// ─── Helpers ────────────────────────────────────────────────────────

function toTitleCase(str: string): string {
Expand DownExpand Up@@ -165,72 +117,6 @@ function loadBundled(templateDir: string, targetDir: string): string[] {
return collected;
}

// ─── Loading: remote (GitHub tarball) ───────────────────────────────

async function downloadTarball(url: string, destFile: string): Promise<void> {
const res = await fetch(url, { redirect: 'follow' });
if (!res.ok || !res.body) {
throw new Error(`Download failed: ${url} (${res.status})`);
}
const out = createWriteStream(destFile);
// node 18+: res.body is a web ReadableStream — pipe via async iterator
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for await (const chunk of res.body as any) {
out.write(chunk);
}
await new Promise<void>((resolve, reject) => {
out.end((err: unknown) => (err ? reject(err as Error) : resolve()));
});
}

async function loadRemote(pkgName: string, targetDir: string): Promise<string[]> {
const tmp = await mkdtemp(path.join(os.tmpdir(), 'create-objectstack-'));
try {
const tarball = path.join(tmp, 'templates.tar.gz');
printStep(`Fetching template "${pkgName}" from ${REMOTE_REPO}@${REMOTE_BRANCH}…`);
await downloadTarball(REMOTE_TARBALL_URL, tarball);

// GitHub tarballs nest everything under `<repo>-<branch>/`. The package we
// want lives at `<repo>-<branch>/packages/<pkgName>/...`. We extract only
// that subtree, stripping the leading 3 path components so the contents
// of `packages/<pkgName>/` land directly in `targetDir`.
fs.mkdirSync(targetDir, { recursive: true });
const collected: string[] = [];
await pipeline(
createReadStream(tarball),
createGunzip(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tar.extract({
cwd: targetDir,
strip: 3,
filter: (p: string) => {
// p looks like: "templates-main/packages/<pkg>/..."
const parts = p.split('/');
return parts[1] === 'packages' && parts[2] === pkgName && parts.length > 3;
},
onentry: (entry: { path: string; type: string }) => {
if (entry.type === 'File') {
// entry.path is the original archive path; strip the 3 leading
// components ("templates-main/packages/<pkg>/") so the reported
// file matches what actually lands on disk.
const parts = entry.path.split('/').slice(3);
if (parts.length > 0) collected.push(parts.join('/'));
}
},
} as any),
);
if (collected.length === 0) {
throw new Error(
`Template "${pkgName}" not found in ${REMOTE_REPO}@${REMOTE_BRANCH} ` +
`(expected packages/${pkgName}/).`,
);
}
return collected;
} finally {
await rm(tmp, { recursive: true, force: true });
}
}

// ─── Field-aware rewrites ───────────────────────────────────────────
//
// The object-name prefix walk moved to rewrite-identity.ts so it can be tested
Expand DownExpand Up@@ -385,12 +271,16 @@ const program = new Command()
.argument('[name]', 'Environment name (defaults to current directory name)')
.option(
'-t, --template <template>',
`Template: ${Object.keys(TEMPLATES).join(', ')}`,
`Template: ${templateNames().join(', ')}`,
'blank',
)
.option('--skip-install', 'Skip dependency installation')
.option('--skip-skills', 'Skip installing ObjectStack AI skills')
.action(async (
// Sync: nothing here awaits any more. The only asynchronous step was the
// remote tarball fetch, and `program.parse()` never awaited the action — so a
// rejection thrown outside the try/catch below would have been an unhandled
// rejection rather than a diagnosed failure.
.action((
name: string | undefined,
options: { template: string; skipInstall?: boolean; skipSkills?: boolean },
) => {
Expand All@@ -401,12 +291,24 @@ const program = new Command()

printHeader('New Environment');

const template = TEMPLATES[options.template];
if (!template) {
printError(`Unknown template: ${options.template}`);
console.log(chalk.dim(` Available: ${Object.keys(TEMPLATES).join(', ')}`));
const lookup = lookupTemplate(options.template);
if (lookup.kind !== 'found') {
if (lookup.kind === 'retired') {
// A returning user typed a name that used to work. Say what happened to
// it — the generic "Unknown template" would read as a typo on their end.
printError(`Template "${lookup.name}" has been retired and is no longer available.`);
console.log(
chalk.dim(
' It was delisted from the ObjectStack template marketplace and is no longer maintained.',
),
);
} else {
printError(`Unknown template: ${lookup.name}`);
}
console.log(chalk.dim(` Available: ${templateNames().join(', ')}`));
process.exit(1);
}
const template = lookup.template;

const cwd = process.cwd();
const projectName = name || path.basename(cwd);
Expand All@@ -431,12 +333,7 @@ const program = new Command()
try {
fs.mkdirSync(targetDir, { recursive: true });

let createdFiles: string[];
if (template.source.kind === 'bundled') {
createdFiles = loadBundled(template.source.dir, targetDir);
} else {
createdFiles = await loadRemote(template.source.pkg, targetDir);
}
const createdFiles = loadBundled(template.source.dir, targetDir);

rewriteProjectIdentity(targetDir, projectName, namespace);

Expand Down
Loading
Loading