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
10 changes: 10 additions & 0 deletions packages/core/src/__tests__/fixtures/stack-manifest.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"schema_version": 1,
"kind": "ashlr-stack-manifest",
"components": [
{ "id": "core", "name": "@ashlr/stack-core", "version": "0.2.2", "distribution": "npm" },
{ "id": "cli", "name": "@ashlr/stack", "version": "0.2.2", "distribution": "npm" },
{ "id": "mcp", "name": "ashlr-stack-mcp", "version": "0.2.2", "distribution": "npm" },
{ "id": "plugin", "name": "stack", "version": "0.1.0", "distribution": "claude-code-plugin" }
]
}
33 changes: 33 additions & 0 deletions packages/core/src/__tests__/stack-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test } from "bun:test";
import cli from "../../../cli/package.json";
import mcp from "../../../mcp/package.json";
import plugin from "../../../plugin/.claude-plugin/plugin.json";
import core from "../../package.json";
import { STACK_MANIFEST, type StackManifest } from "../index.ts";
import fixture from "./fixtures/stack-manifest.v1.json";

describe("Stack ecosystem manifest v1", () => {
test("public export preserves the fixture's JSON wire contract", () => {
const manifest: StackManifest = STACK_MANIFEST;
expect(JSON.parse(JSON.stringify(manifest))).toEqual(fixture);
});

test("every distributable matches its source name and version", () => {
const metadata = { core, cli, mcp, plugin };
const manifest: StackManifest = STACK_MANIFEST;
expect(Object.keys(metadata).sort()).toEqual(manifest.components.map(({ id }) => id).sort());
for (const component of manifest.components) {
expect(component.name).toBe(metadata[component.id].name);
expect(component.version).toBe(metadata[component.id].version);
expect(component.version).toMatch(/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/);
}
});

test("shared metadata cannot be mutated by consumers", () => {
expect(Object.isFrozen(STACK_MANIFEST)).toBe(true);
expect(Object.isFrozen(STACK_MANIFEST.components)).toBe(true);
for (const component of STACK_MANIFEST.components) {
expect(Object.isFrozen(component)).toBe(true);
}
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./config.ts";
export { STACK_MANIFEST, type StackManifest, type StackComponent } from "./stack-manifest.ts";
export {
isEnabled as isTelemetryEnabled,
promptFirstRun as promptTelemetryFirstRun,
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/stack-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** A distributable Stack component, rather than an installed companion tool. */
export interface StackComponent {
readonly id: "core" | "cli" | "mcp" | "plugin";
readonly name: string;
readonly version: string;
readonly distribution: "npm" | "claude-code-plugin";
}

/**
* Source release metadata for ecosystem consumers. Versions do not establish
* registry publication or describe tools installed on the current machine.
* Additive fields may stay in v1; removals or renames require a schema bump.
*/
export interface StackManifest {
readonly schema_version: 1;
readonly kind: "ashlr-stack-manifest";
readonly components: readonly StackComponent[];
}

/**
* Import from @ashlr/stack-core. This JSON-safe export performs no filesystem,
* network, credential or subprocess access. Keep versions in sync on release;
* the fixture contract test checks the package and plugin metadata for drift.
*/
export const STACK_MANIFEST = Object.freeze({
schema_version: 1,
kind: "ashlr-stack-manifest",
components: Object.freeze([
Object.freeze({ id: "core", name: "@ashlr/stack-core", version: "0.2.2", distribution: "npm" }),
Object.freeze({ id: "cli", name: "@ashlr/stack", version: "0.2.2", distribution: "npm" }),
Object.freeze({ id: "mcp", name: "ashlr-stack-mcp", version: "0.2.2", distribution: "npm" }),
Object.freeze({
id: "plugin",
name: "stack",
version: "0.1.0",
distribution: "claude-code-plugin",
}),
]),
} as const) satisfies StackManifest;
Loading