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
2 changes: 1 addition & 1 deletion .okf/architecture/catalog-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ generated: { by: codex, at: "2026-09-27T21:00:00+02:00" }

# Catalog distribution and freshness

Status: Accepted direction, not yet implemented. Current code still bundles catalog definitions. GitHub issues own implementation scope, sequencing, and acceptance evidence; this document owns the reusable decisions and their consequences.
Status: Accepted direction. Local work for #249 now composes and validates explicitly supplied definitions, then injects one catalog through scaffold services. The CLI and Recipe Builder still use a temporary bundled adapter. HTTP loading, caching, publication, and deployed behavior remain unimplemented. GitHub issues own implementation scope, sequencing, and acceptance evidence; this document owns the reusable decisions and their consequences.

## Separate content delivery from engine releases

Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
| `bun run type-check` | Run TypeScript checks across workspaces |
| `bun run test` | Run workspace tests through Turbo + Vitest |
| `bun run test --filter=<package>` | Run tests for a specific workspace |
| `bun run catalog:reset-workspace` | Build the local authoring catalog workspace |
| `bun run catalog:diff-workspace` | Compare generated authoring files |
| `bun run catalog:validate-workspace` | Validate generated authoring files |

## Tech Stack

Expand Down Expand Up @@ -65,6 +68,8 @@ Selection ──> Blueprint ┬─> Plan ──> Apply ──> ApplyResult

The CLI (`apps/cli`) orchestrates this flow using shared packages in `packages/*`. Users run `stack-effect init` to create a project and `stack-effect add` to incrementally add targets (client, server, cli, package) with modules (features).

The root `catalog:*` scripts use `apps/cli/src/authoring.ts` and local definitions. Published `stack-effect catalog workspace` command names remain available through the CLI entrypoint.

## Core Priorities

1. Deterministic behavior: identical inputs should produce identical blueprint and plan outputs.
Expand Down
15 changes: 8 additions & 7 deletions apps/cli/e2e/matrix.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, layer } from "@effect/vitest";
import { CatalogService } from "@repo/catalog";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import { ModuleId, TargetKind } from "@repo/domain/Catalog";
import { Effect } from "effect";
import { CLI } from "./harness";
Expand Down Expand Up @@ -71,13 +72,13 @@ const defaultTargetNames = new Map([

const catalogModules = Effect.runSync(
Effect.map(CatalogService, (catalog) => catalog.getModules()).pipe(
Effect.provide(CatalogService.layer),
Effect.provide(BundledCatalogLayer),
),
);
const getCapabilityProviders = Effect.runSync(
Effect.map(CatalogService, (catalog) =>
catalog.getCapabilityProviders.bind(catalog),
).pipe(Effect.provide(CatalogService.layer)),
).pipe(Effect.provide(BundledCatalogLayer)),
);
const catalogModulesById = new Map(
catalogModules.map((module) => [module.id, module]),
Expand Down Expand Up @@ -194,7 +195,7 @@ const buildMatrix = Effect.gen(function* () {
});

const matrix = Effect.runSync(
buildMatrix.pipe(Effect.provide(CatalogService.layer)),
buildMatrix.pipe(Effect.provide(BundledCatalogLayer)),
);

const individualModuleEntries = matrix.filter(
Expand Down Expand Up @@ -305,7 +306,7 @@ const buildChildrenMatrix = Effect.gen(function* () {
});

const childrenMatrix = Effect.runSync(
buildChildrenMatrix.pipe(Effect.provide(CatalogService.layer)),
buildChildrenMatrix.pipe(Effect.provide(BundledCatalogLayer)),
);

const buildCapabilityMatrix = Effect.gen(function* () {
Expand Down Expand Up @@ -339,7 +340,7 @@ const buildCapabilityMatrix = Effect.gen(function* () {
});

const capabilityMatrix = Effect.runSync(
buildCapabilityMatrix.pipe(Effect.provide(CatalogService.layer)),
buildCapabilityMatrix.pipe(Effect.provide(BundledCatalogLayer)),
);

const buildFullStackMatrix = Effect.gen(function* () {
Expand Down Expand Up @@ -385,7 +386,7 @@ const buildFullStackMatrix = Effect.gen(function* () {
});

const fullStackMatrix = Effect.runSync(
buildFullStackMatrix.pipe(Effect.provide(CatalogService.layer)),
buildFullStackMatrix.pipe(Effect.provide(BundledCatalogLayer)),
);

const buildMaximalClientMatrix = Effect.gen(function* () {
Expand All @@ -412,7 +413,7 @@ const buildMaximalClientMatrix = Effect.gen(function* () {
});

const maximalClientMatrix = Effect.runSync(
buildMaximalClientMatrix.pipe(Effect.provide(CatalogService.layer)),
buildMaximalClientMatrix.pipe(Effect.provide(BundledCatalogLayer)),
);

const expectInstallPasses = (project: {
Expand Down
15 changes: 15 additions & 0 deletions apps/cli/src/authoring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NodeRuntime } from "@effect/platform-node";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import { Effect, Layer } from "effect";
import { cliProgram } from "./cliProgram";
import { PlatformLayer } from "./runtime";
import { StackEffectServicesLayer } from "./services";

const AuthoringLayer = StackEffectServicesLayer.pipe(
Layer.provideMerge(BundledCatalogLayer),
Layer.provideMerge(PlatformLayer),
);

NodeRuntime.runMain(cliProgram.pipe(Effect.provide(AuthoringLayer)), {
disableErrorReporting: true,
});
43 changes: 43 additions & 0 deletions apps/cli/src/cliProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Cause, Console, Effect } from "effect";
import { Ansi, Box } from "effect-boxes";
import { Command } from "effect/unstable/cli";
import pkg from "../package.json";
import { stackEffectCommand } from "./command";

export const cliProgram = stackEffectCommand.pipe(
Command.run({ version: pkg.version }),
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) {
const message = Box.vsep(
[
Box.text("Interrupted.").pipe(
Box.annotate(Ansi.combine(Ansi.bold, Ansi.yellow)),
),
Box.text("Goodbye! Come back when you're ready to stack."),
],
1,
Box.center1,
).pipe(
Box.pad(0, 1),
Box.border("rounded", { annotation: Ansi.yellow }),
Box.moveDown(1),
);
return Console.log(`\n${Box.renderPrettySync(message)}`);
}

const message = Cause.prettyErrors(cause)
.map((error) => error.message)
.filter((message) => message.length > 0)
.join("\n");

return Console.error(
message.length > 0 ? message : Cause.pretty(cause),
).pipe(
Effect.andThen(
Effect.sync(() => {
process.exitCode = 1;
}),
),
);
}),
);
4 changes: 2 additions & 2 deletions apps/cli/src/commands/add.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@effect/vitest";
import { CatalogService } from "@repo/catalog";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import { ModuleId, TargetKind } from "@repo/domain/Catalog";
import { Effect } from "effect";
import { type CollectedTarget, resolveCapabilities } from "./add";
Expand Down Expand Up @@ -44,6 +44,6 @@ describe("add capability resolution", () => {
modules: ["package-db-sqlite"],
confirmed: false,
});
}).pipe(Effect.provide(CatalogService.layer)),
}).pipe(Effect.provide(BundledCatalogLayer)),
);
});
2 changes: 2 additions & 0 deletions apps/cli/src/docs/CliReference.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NodeServices } from "@effect/platform-node";
import { describe, expect, it } from "@effect/vitest";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import { Effect, Layer } from "effect";
import pkg from "../../package.json";
import { stackEffectCommand } from "../command";
Expand All @@ -8,6 +9,7 @@ import { collectCliReference, validateCliReference } from "./CliReference";
import { renderCliReferencePages } from "./CliReferenceMarkdown";

const TestLayer = StackEffectServicesLayer.pipe(
Layer.provideMerge(BundledCatalogLayer),
Layer.provideMerge(NodeServices.layer),
);

Expand Down
50 changes: 5 additions & 45 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,8 @@
import { NodeRuntime } from "@effect/platform-node";
import { Cause, Console, Effect } from "effect";
import { Ansi, Box } from "effect-boxes";
import { Command } from "effect/unstable/cli";
import pkg from "../package.json";
import { stackEffectCommand } from "./command";
import { Effect } from "effect";
import { cliProgram } from "./cliProgram";
import { StackEffectLayer } from "./runtime";

const program = stackEffectCommand.pipe(
Command.run({ version: pkg.version }),
Effect.provide(StackEffectLayer),
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) {
const message = Box.vsep(
[
Box.text("Interrupted.").pipe(
Box.annotate(Ansi.combine(Ansi.bold, Ansi.yellow)),
),
Box.text("Goodbye! Come back when you're ready to stack."),
],
1,
Box.center1,
).pipe(
Box.pad(0, 1),
Box.border("rounded", { annotation: Ansi.yellow }),
Box.moveDown(1),
);
return Console.log(`\n${Box.renderPrettySync(message)}`);
}

const message = Cause.prettyErrors(cause)
.map((error) => error.message)
.filter((message) => message.length > 0)
.join("\n");

return Console.error(
message.length > 0 ? message : Cause.pretty(cause),
).pipe(
Effect.andThen(
Effect.sync(() => {
process.exitCode = 1;
}),
),
);
}),
);

NodeRuntime.runMain(program, { disableErrorReporting: true });
NodeRuntime.runMain(cliProgram.pipe(Effect.provide(StackEffectLayer)), {
disableErrorReporting: true,
});
4 changes: 3 additions & 1 deletion apps/cli/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { BunServices } from "@effect/platform-bun";
import { NodeServices } from "@effect/platform-node";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import { Config, Effect, Layer } from "effect";
import { StackEffectServicesLayer } from "./services";

const CliConfig = Config.all({
TARGET: Config.Literals(["bun", "node"]).pipe(Config.withDefault("node")),
});

const PlatformLayer = Layer.unwrap(
export const PlatformLayer = Layer.unwrap(
Effect.gen(function* () {
const config = yield* CliConfig;
return config.TARGET === "bun" ? BunServices.layer : NodeServices.layer;
}),
);

export const StackEffectLayer = StackEffectServicesLayer.pipe(
Layer.provideMerge(BundledCatalogLayer),
Layer.provideMerge(PlatformLayer),
);
4 changes: 2 additions & 2 deletions apps/cli/src/service/RecipeService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, describe, it } from "@effect/vitest";
import { CatalogService } from "@repo/catalog";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import {
ModuleCapability,
ModuleId,
Expand Down Expand Up @@ -34,7 +34,7 @@ const testConfig = new StackConfig(testConfigFields);

const makeTestLayer = (defaults: StackConfig) =>
RecipeService.layer.pipe(
Layer.provide(CatalogService.layer),
Layer.provide(BundledCatalogLayer),
Layer.provide(Layer.succeed(StackConfigDefaults, defaults)),
);

Expand Down
3 changes: 1 addition & 2 deletions apps/cli/src/services.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { CatalogService } from "@repo/catalog";
import {
ApplyPreviewService,
ApplyService,
Expand All @@ -24,4 +23,4 @@ export const StackEffectServicesLayer = Layer.mergeAll(
ConfigureService.layer,
RecipeService.layer,
ScaffoldPipeline.layer,
).pipe(Layer.provideMerge(CatalogService.layer));
);
2 changes: 1 addition & 1 deletion apps/docs/app/content/reference/cli/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# CLI reference

Commands, arguments, options, and examples for `stack-effect v0.14.0`. These pages are generated from the current Effect CLI command tree.
Commands, arguments, options, and examples for `stack-effect v0.15.0`. These pages are generated from the current Effect CLI command tree.

## Commands

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import * as BrowserWorkerRunner from "@effect/platform-browser/BrowserWorkerRunner";
import { CatalogService } from "@repo/catalog";
import { BundledCatalogLayer } from "@repo/catalog/authoring";
import { ModuleCategory } from "@repo/domain/Catalog";
import {
RecipePreviewService,
Expand Down Expand Up @@ -59,8 +60,8 @@ const RecipeBuilderRpcHandlersLive = RecipeBuilderRpc.toLayer(
});
}),
).pipe(
Layer.provide(CatalogService.layer),
Layer.provide(RecipePreviewService.layer),
Layer.provide(BundledCatalogLayer),
);

const WorkerLive = RpcServer.layer(RecipeBuilderRpc, {
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
},
"scripts": {
"build": "turbo run build",
"catalog:diff-workspace": "bun run start -- catalog workspace diff --root workspace/catalog-built",
"catalog:reset-workspace": "bun run start -- catalog workspace reset --root workspace/catalog-built",
"catalog:validate-workspace": "bun run start -- catalog workspace validate --root workspace/catalog-built",
"catalog:diff-workspace": "bun apps/cli/src/authoring.ts catalog workspace diff --root workspace/catalog-built",
"catalog:reset-workspace": "bun apps/cli/src/authoring.ts catalog workspace reset --root workspace/catalog-built",
"catalog:validate-workspace": "bun apps/cli/src/authoring.ts catalog workspace validate --root workspace/catalog-built",
"clean": "turbo run clean && git clean -xdf node_modules .cache .turbo dist tsconfig.tsbuildinfo playwright-report test-results",
"dev": "turbo run dev",
"docs:check": "bun run docs:generate && git diff --exit-code -- apps/docs/app/content/reference/cli",
Expand Down
3 changes: 2 additions & 1 deletion packages/catalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./authoring": "./src/authoring.ts"
},
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
3 changes: 2 additions & 1 deletion packages/catalog/src/CatalogService.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { assert, layer } from "@effect/vitest";
import { ModuleId } from "@repo/domain/Catalog";
import { Effect } from "effect";
import { BundledCatalogLayer } from "./authoring";
import { CatalogService } from "./CatalogService";

layer(CatalogService.layer)("CatalogService", (it) => {
layer(BundledCatalogLayer)("CatalogService", (it) => {
it.effect(
"should expose module incompatibilities when building the public catalog tree",
() =>
Expand Down
Loading
Loading