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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main, "cursor/**"]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.28.2

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm

- name: Install
run: pnpm install --frozen-lockfile

- name: Test
run: pnpm test
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Allow-list: ignore everything, then un-ignore tracked paths.
*
!.gitignore
!LICENSE
!README.md
!CONTRIBUTING.md
!package.json
!pnpm-lock.yaml
!tsconfig.json
!manifests/
!manifests/**
!src/
!src/**
!test/
!test/**
!docs/
!docs/**
!.github/
!.github/**
25 changes: 25 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Contributing to shell-framework

This repo is the **extension host** for OpenShellOrg. Architecture and thesis live in [shell-architecture](https://github.com/openshellorg/shell-architecture).

## Layout

- `src/` — host implementation (manifest loading, registration, core discovery)
- `manifests/core/` — JSON manifests that **point at** upstream repos; never vendor extension source here
- `test/` — Node test runner checks against built `dist/`

## Adding a core extension

1. Add `manifests/core/<name>.manifest.json` following the schema in [`docs/extension-manifest.md`](docs/extension-manifest.md).
2. Register the file path in `manifests/core.index.json`.
3. Extend tests if the extension has required fields beyond the base schema.

## Development

```bash
pnpm install
pnpm build
pnpm test
```

Pull requests should keep CI green (`pnpm test` runs typecheck, build, and tests).
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 OpenShellOrg contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# shell-framework

OpenShellOrg modular extension host (scaffold in progress).
**OpenShellOrg modular extension host** — manifests, discovery, and registration for shell extensions. This repository implements the host stub that loads extension manifests and registers core and user-supplied extensions.

Canon architecture: https://github.com/openshellorg/shell-architecture
## Dual-repo model

OpenShellOrg splits **architecture** from **implementation**:

| Repository | Role |
|------------|------|
| [**shell-architecture**](https://github.com/openshellorg/shell-architecture) | **Canon** — thesis, Tool Runs, command channels, Antora docs, diagrams |
| **shell-framework** (this repo) | **Extension host** — manifest schema, core extension registry, discovery, host APIs |

Do not duplicate architecture docs here. When you need design rationale (structured pipelines, SOS relationship, host identity, env refresh plans), use [shell-architecture](https://github.com/openshellorg/shell-architecture).

## What lives here

- **`manifests/core/`** — pointers to first-party core extensions (repos/packages), not vendored source
- **`src/`** — TypeScript extension host: load manifests, register extensions, discover bundled core set
- **Tests + CI** — build and verify core registration (e.g. Prohelp) without cloning extension repos

## Core extensions

Core extensions are registered **by manifest only**. The host does not embed their code.

| Extension | Manifest | Upstream |
|-----------|----------|----------|
| Prohelp | [`manifests/core/prohelp.manifest.json`](manifests/core/prohelp.manifest.json) | [openshellorg/prohelp](https://github.com/openshellorg/prohelp) · CLI: [prohelp-cli](https://github.com/openshellorg/prohelp-cli) |

## Quick start

```bash
pnpm install
pnpm build
pnpm test
```

Programmatic bootstrap:

```ts
import { bootstrapExtensionHost } from "@openshellorg/shell-framework";

const host = await bootstrapExtensionHost();
console.log(host.listCore()); // includes openshellorg/prohelp
```

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for layout and manifest conventions.

## Related projects

- [shell-architecture](https://github.com/openshellorg/shell-architecture) — canon docs
- [prohelp](https://github.com/openshellorg/prohelp) — structured help / gutter tooling (first core extension)
- [docs](https://github.com/openshellorg/docs) — SOS certification and org docs site

## License

MIT — see [LICENSE](LICENSE).
26 changes: 26 additions & 0 deletions docs/extension-manifest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Extension manifest

Core and third-party extensions are described by JSON manifests. The host loads manifests from disk and registers them; it does not download or embed extension repositories.

## Required fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Stable identifier (e.g. `openshellorg/prohelp`) |
| `name` | string | Display name |
| `repository` | string | Canonical source repo URL |
| `core` | boolean | When `true`, discovered via `manifests/core/` |

## Optional fields

| Field | Type | Description |
|-------|------|-------------|
| `version` | string | Manifest or pinned extension version hint |
| `package` | string | npm package name when published |
| `cli` | object | CLI-related pointers |
| `cli.repository` | string | CLI repo URL (e.g. prohelp-cli) |
| `cli.binary` | string | Expected CLI command name |

## Example (Prohelp)

See [`manifests/core/prohelp.manifest.json`](../manifests/core/prohelp.manifest.json).
6 changes: 6 additions & 0 deletions manifests/core.index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"description": "Ordered list of core extension manifest paths relative to the repository root.",
"manifests": [
"manifests/core/prohelp.manifest.json"
]
}
12 changes: 12 additions & 0 deletions manifests/core/prohelp.manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "openshellorg/prohelp",
"name": "Prohelp",
"version": "0.0.0",
"core": true,
"repository": "https://github.com/openshellorg/prohelp",
"package": "@openshellorg/prohelp",
"cli": {
"repository": "https://github.com/openshellorg/prohelp-cli",
"binary": "prohelp"
}
}
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@openshellorg/shell-framework",
"version": "0.0.0",
"description": "OpenShellOrg modular extension host — manifests, discovery, and registration",
"license": "MIT",
"type": "module",
"packageManager": "pnpm@10.28.2",
"engines": {
"node": ">=20"
},
"files": [
"dist",
"manifests",
"README.md",
"LICENSE"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./manifests/core/*": "./manifests/core/*"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "pnpm build && node --test test/**/*.test.js",
"prepublishOnly": "pnpm test"
},
"repository": {
"type": "git",
"url": "https://github.com/openshellorg/shell-framework.git"
},
"bugs": {
"url": "https://github.com/openshellorg/shell-framework/issues"
},
"homepage": "https://github.com/openshellorg/shell-framework#readme",
"devDependencies": {
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
}
}
39 changes: 39 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions src/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { readFile } from "node:fs/promises";
import path from "node:path";

import {
type CoreIndex,
type ExtensionManifest,
ManifestValidationError,
validateExtensionManifest,
} from "./types.js";

export async function loadJsonFile(filePath: string): Promise<unknown> {
const raw = await readFile(filePath, "utf8");
try {
return JSON.parse(raw) as unknown;
} catch {
throw new ManifestValidationError(`Invalid JSON: ${filePath}`);
}
}

export async function loadExtensionManifest(
manifestPath: string,
): Promise<ExtensionManifest> {
const parsed = await loadJsonFile(manifestPath);
return validateExtensionManifest(parsed, manifestPath);
}

export async function loadCoreIndex(indexPath: string): Promise<CoreIndex> {
const parsed = await loadJsonFile(indexPath);
if (parsed === null || typeof parsed !== "object") {
throw new ManifestValidationError(`${indexPath}: expected an object`);
}
const record = parsed as Record<string, unknown>;
if (!Array.isArray(record.manifests)) {
throw new ManifestValidationError(`${indexPath}: "manifests" must be an array`);
}
const manifests = record.manifests.map((entry, i) => {
if (typeof entry !== "string" || entry.length === 0) {
throw new ManifestValidationError(
`${indexPath}: manifests[${i}] must be a non-empty string path`,
);
}
return entry;
});
const description =
typeof record.description === "string" ? record.description : undefined;
return { description, manifests };
}

export interface DiscoverCoreOptions {
/** Repository root (defaults to cwd). */
rootDir?: string;
/** Path to core index JSON relative to rootDir. */
coreIndexPath?: string;
}

/**
* Resolve and load all core extension manifests listed in manifests/core.index.json.
*/
export async function discoverCoreExtensions(
options: DiscoverCoreOptions = {},
): Promise<Array<{ manifest: ExtensionManifest; manifestPath: string }>> {
const rootDir = options.rootDir ?? process.cwd();
const coreIndexPath =
options.coreIndexPath ?? path.join("manifests", "core.index.json");
const indexAbs = path.resolve(rootDir, coreIndexPath);
const index = await loadCoreIndex(indexAbs);

const results: Array<{ manifest: ExtensionManifest; manifestPath: string }> =
[];
for (const relativePath of index.manifests) {
const manifestPath = path.resolve(rootDir, relativePath);
const manifest = await loadExtensionManifest(manifestPath);
if (manifest.core !== true) {
throw new ManifestValidationError(
`${manifestPath}: core index entries must set "core": true`,
);
}
results.push({ manifest, manifestPath });
}
return results;
}
Loading
Loading