feat(cli): replace prototype with SDK runner

This commit is contained in:
Ubuntu 2026-06-08 04:18:18 +00:00
parent 116b30454a
commit 8a9b788f76
105 changed files with 27064 additions and 119 deletions

View File

@ -20,8 +20,8 @@ packed tarballs.
- Package-author CLIs and project scaffolds - Package-author CLIs and project scaffolds
Matrix SDK is provider-neutral. It does not require a managed platform account, Matrix SDK is provider-neutral. It does not require a managed platform account,
device-link ceremony, host-link implementation, managed host service, local machine-link ceremony, managed host service, local platform install, Postgres
platform install, Postgres database, or provider-owned broker authority. A database, or provider-owned broker authority. A
provider overlay can issue broker credentials and host runtimes on top of the provider overlay can issue broker credentials and host runtimes on top of the
SDK, but the SDK itself must remain usable against any compatible broker. SDK, but the SDK itself must remain usable against any compatible broker.
@ -134,27 +134,22 @@ The SDK CLI package owns the package-author `matrix` binary.
```bash ```bash
matrix init [dir] matrix init [dir]
matrix add actor <name>
matrix actor <name> matrix actor <name>
matrix configure --key <key> --cloud <url> --space <space> matrix actor run ./dist/MyActor.js --mount demo.actor --nats-url nats://127.0.0.1:4222 --root demo --check-op ping
matrix run <entry> --mount <mount> matrix package run . --nats-url nats://127.0.0.1:4222 --root demo --check-op ping
matrix invoke <mount> <op> [json] matrix config list
``` ```
The current SDK CLI acceptance proof is: The current SDK CLI proof is:
```bash ```bash
pnpm prove:cli-uat pnpm prove:cli
``` ```
The command name uses `uat`, but its scope is only the SDK package-author CLI
flow. It is not managed-platform product UAT, browser signup/login UAT,
API-key UI UAT, or live deployment proof.
The CLI is intended to let a package author initialize a folder, add actors, The CLI is intended to let a package author initialize a folder, add actors,
configure broker access, run actors, and invoke them without importing provider configure broker access, and run/check actors without importing provider
overlay packages into the SDK layer. Managed-host promotion is optional and is overlay packages into the SDK layer. Managed-host promotion and provider login
only exercised when `MATRIX_SDK_RUNTIME_HOST_BIN` is supplied. belong in overlay packages, not in the SDK CLI core.
## Demos ## Demos
@ -165,27 +160,14 @@ pnpm demo:standalone
``` ```
This builds the SDK and `examples/standalone-greeter`, starts the actor with This builds the SDK and `examples/standalone-greeter`, starts the actor with
`matrix run`, calls it with `matrix invoke`, and shuts it down. `matrix package run`, calls it with `--check-op` over a real local NATS broker,
and shuts it down. It does not use a provider account, machine link, managed
host service, or local HTTP control server.
HiveCast API-key login/config demo: Provider API-key login/config work has been moved out of the active SDK demo set
and preserved under `archive/sdk-provider-overlay-candidates/`. It belongs to a
```bash provider overlay that can write the same broker config shape consumed by the SDK
export MATRIX_CLOUD=https://dev.hivecast.ai CLI.
export MATRIX_SPACE=<your-space-root>
export MATRIX_API_KEY=<your-api-key>
pnpm demo:hivecast-login
```
This validates the key with HiveCast and writes the SDK package-local
`.matrix/config.json` plus `.matrix/credentials/matrix-api-key.json` shape
without printing or committing the key. By default the demo uses a temporary
home and deletes it after success.
Current boundary: this HiveCast demo proves SDK-side key validation and config
storage. It does not yet prove a package running directly on a HiveCast-issued
broker credential. That requires the provider-owned login/credential-exchange
slice so `matrix login` can replace manual `matrix configure` and issue/store
the broker credentials needed by the SDK transport.
## Repository Layout ## Repository Layout
@ -201,9 +183,7 @@ packages/
sdk/ sdk/
scripts/ scripts/
demo:standalone demo:standalone
demo:hivecast-login
prove-external-consumer.mjs prove-external-consumer.mjs
prove-sdk-cli-uat.mjs
``` ```
## Current Status ## Current Status
@ -216,13 +196,15 @@ Done and proven:
- A fresh clone passes `pnpm install --frozen-lockfile && pnpm prove`. - A fresh clone passes `pnpm install --frozen-lockfile && pnpm prove`.
- A temporary external consumer can install packed SDK tarballs, compile, start - A temporary external consumer can install packed SDK tarballs, compile, start
a `MatrixActor`, and call it through Matrix runtime/transport. a `MatrixActor`, and call it through Matrix runtime/transport.
- `matrix package run` can mount and check a package against an explicit NATS
broker without provider-specific API key exchange.
Still planned: Still planned:
- Provider-neutral credential resolution for `matrix configure` and SDK users. - A polished provider-neutral `matrix configure` UX for broker URL/root/JWT/seed
- Provider login commands that write the same config shape as files.
`matrix configure`. - Provider login commands in overlay packages that write the same broker config
- Direct broker mode with explicit broker URL/JWT/seed credentials. shape consumed by SDK direct-run commands.
- Managed-provider mode where an overlay issues scoped broker credentials. - Managed-provider mode where an overlay issues scoped broker credentials.
Publishing is intentionally not part of this repository's normal developer Publishing is intentionally not part of this repository's normal developer

View File

@ -0,0 +1,23 @@
# @open-matrix/cli
Package-author CLI for the Matrix SDK layer.
This package owns the SDK/package-author `matrix` binary. It does not depend on
provider overlay packages, managed host packages, platform system actors,
device-link implementations, or broker-authority/signing packages.
Initial command surface:
```bash
matrix init [dir]
matrix add actor <name>
matrix actor <name>
matrix configure --key <key> --cloud <url> --space <space>
matrix run <entry> --mount <mount>
matrix invoke <mount> <op> [json]
```
`matrix run` starts a standalone actor runner in the current folder.
`matrix invoke` sends a Matrix request envelope to that runner through its local
control port. Managed host promotion belongs to provider overlays that consume
the SDK; it is not an SDK-layer requirement.

View File

@ -0,0 +1,40 @@
{
"name": "@open-matrix/cli",
"version": "0.1.0",
"description": "Package-author Matrix CLI for SDK projects.",
"type": "module",
"private": true,
"bin": {
"matrix": "./dist/bin/matrix.js"
},
"exports": {
".": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.js",
"default": "./dist/cli.js"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"package.json",
"README.md",
"CONTRACT.md"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "node --test dist/__tests__/*.test.js",
"type-check": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@open-matrix/core": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.0.10",
"typescript": "^5.7.0"
},
"license": "MIT",
"engines": {
"node": ">=20"
}
}

View File

@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"strict": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"noEmit": false,
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"dist",
"node_modules"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,178 @@
import { strict as assert } from 'node:assert';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
import { saveHiveCastHostLink } from '../../src/utils/hivecast-link-store.js';
describe('HiveCast link store', () => {
it('keeps a normal linked Device on local NATS when the saved local-owner snapshot is missing', () => {
const root = mkdtempSync(join(tmpdir(), 'mx-hivecast-link-store-'));
try {
const matrixHome = join(root, '.matrix');
const hostConfigPath = join(matrixHome, 'host.json');
mkdirSync(matrixHome, { recursive: true });
writeFileSync(hostConfigPath, `${JSON.stringify({
kind: 'MatrixHostConfig',
version: 1,
home: matrixHome,
transport: {
root: 'richard-santomauro-nimbletec-com',
authorityRoot: 'richard-santomauro-nimbletec-com',
addressRoot: 'richard-santomauro-nimbletec-com',
nats: {
mode: 'external',
url: 'nats://local.hivecast.ai:4222',
wsUrl: 'wss://local.hivecast.ai/nats-ws',
credentialsRef: 'file:/var/lib/hivecast/credentials/hivecast-nats.json',
},
},
}, null, 2)}\n`);
saveHiveCastHostLink({
cwd: root,
matrixHome,
cloudUrl: 'https://local.hivecast.ai',
authorityRoot: 'richard-santomauro-nimbletec-com',
deviceRegistryRoot: 'richard-santomauro-nimbletec-com',
hostLinkId: 'hostlink_test',
hostId: 'install_test',
principalId: 'principal_test',
hostName: 'test-device',
deviceSlug: 'test-device',
routeKey: 'richard-santomauro-nimbletec-com',
publicNamespace: 'richard-santomauro-nimbletec-com',
spaceId: 'space_test',
authorityCoordinator: false,
nats: {
url: 'nats://local.hivecast.ai:4222',
wsUrl: 'wss://local.hivecast.ai/nats-ws',
},
natsCredentials: {
jwt: 'jwt',
seed: 'seed',
},
});
const hostConfig = JSON.parse(readFileSync(hostConfigPath, 'utf8')) as {
readonly transport?: {
readonly root?: string;
readonly authorityRoot?: string;
readonly nats?: {
readonly url?: string;
readonly wsUrl?: string;
readonly credentialsRef?: string;
readonly binaryPath?: string;
};
};
};
assert.equal(hostConfig.transport?.root, 'richard-santomauro-nimbletec-com');
assert.equal(hostConfig.transport?.authorityRoot, 'richard-santomauro-nimbletec-com');
assert.equal(hostConfig.transport?.nats?.url, 'nats://127.0.0.1:4222');
assert.equal(hostConfig.transport?.nats?.wsUrl, 'ws://127.0.0.1:4223');
assert.equal(hostConfig.transport?.nats?.credentialsRef, undefined);
assert.equal(hostConfig.transport?.nats?.binaryPath, undefined);
assert.equal(Object.prototype.hasOwnProperty.call(hostConfig, 'natsTopology'), false);
assert.equal(existsSync(join(matrixHome, 'credentials', 'hivecast-nats.creds')), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('configures linked Device leaf-node transport when the platform exchange advertises it', () => {
const root = mkdtempSync(join(tmpdir(), 'mx-hivecast-link-store-leaf-'));
try {
const matrixHome = join(root, '.matrix');
const hostConfigPath = join(matrixHome, 'host.json');
mkdirSync(matrixHome, { recursive: true });
writeFileSync(hostConfigPath, `${JSON.stringify({
kind: 'MatrixHostConfig',
version: 1,
home: matrixHome,
transport: {
root: 'local-install-test',
authorityRoot: 'local-install-test',
addressRoot: 'local-install-test',
nats: {
mode: 'external',
url: 'nats://127.0.0.1:4222',
wsUrl: 'ws://127.0.0.1:4223',
},
},
}, null, 2)}\n`);
saveHiveCastHostLink({
cwd: root,
matrixHome,
cloudUrl: 'https://local.hivecast.ai',
authorityRoot: 'richard-santomauro-nimbletec-com',
deviceRegistryRoot: 'richard-santomauro-nimbletec-com',
hostLinkId: 'hostlink_test',
hostId: 'install_test',
principalId: 'principal_test',
hostName: 'test-device',
deviceSlug: 'test-device',
routeKey: 'richard-santomauro-nimbletec-com',
publicNamespace: 'richard-santomauro-nimbletec-com',
spaceId: 'space_test',
authorityCoordinator: false,
nats: {
url: 'nats://local.hivecast.ai:4222',
wsUrl: 'wss://local.hivecast.ai/nats-ws',
leafnodeUrl: 'nats://local.hivecast.ai:7422',
},
natsCredentials: {
jwt: 'jwt',
seed: 'seed',
},
});
const hostConfig = JSON.parse(readFileSync(hostConfigPath, 'utf8')) as {
readonly transport?: {
readonly root?: string;
readonly authorityRoot?: string;
readonly nats?: {
readonly url?: string;
readonly wsUrl?: string;
readonly credentialsRef?: string;
readonly binaryPath?: string;
};
};
readonly natsTopology?: {
readonly mode?: string;
readonly clientUrl?: string;
readonly wsUrl?: string;
readonly leafnodes?: readonly {
readonly url?: string;
readonly credentialsRef?: string;
}[];
};
};
const link = JSON.parse(readFileSync(join(matrixHome, 'credentials', 'hivecast-link.json'), 'utf8')) as {
readonly credentials?: {
readonly natsLeafnodeCredentialRef?: string;
};
};
const leafCredentials = readFileSync(join(matrixHome, 'credentials', 'hivecast-nats.creds'), 'utf8');
assert.equal(hostConfig.transport?.root, 'richard-santomauro-nimbletec-com');
assert.equal(hostConfig.transport?.authorityRoot, 'richard-santomauro-nimbletec-com');
assert.equal(hostConfig.transport?.nats?.url, 'nats://127.0.0.1:4222');
assert.equal(hostConfig.transport?.nats?.wsUrl, 'ws://127.0.0.1:4223');
assert.equal(hostConfig.transport?.nats?.credentialsRef, undefined);
assert.equal(hostConfig.transport?.nats?.binaryPath, undefined);
assert.equal(hostConfig.natsTopology?.mode, 'leafnode');
assert.equal(hostConfig.natsTopology?.clientUrl, 'nats://127.0.0.1:4222');
assert.equal(hostConfig.natsTopology?.wsUrl, 'ws://127.0.0.1:4223');
assert.equal(hostConfig.natsTopology?.leafnodes?.[0]?.url, 'nats://local.hivecast.ai:7422');
assert.equal(hostConfig.natsTopology?.leafnodes?.[0]?.credentialsRef, 'file:credentials/hivecast-nats.creds');
assert.equal(link.credentials?.natsLeafnodeCredentialRef, 'file:credentials/hivecast-nats.creds');
assert.match(leafCredentials, /-----BEGIN NATS USER JWT-----\njwt\n------END NATS USER JWT------/);
assert.match(leafCredentials, /-----BEGIN USER NKEY SEED-----\nseed\n------END USER NKEY SEED------/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import path from 'node:path';
import { configExplainCommand } from '../../src/commands/config.js';
describe('mx config command', () => {
it('classifies package declarations as shared direct and managed inputs', async () => {
const result = await configExplainCommand('packages/chat/matrix.json', {
cwd: '/repo',
});
assert.equal(result.kind, 'package-declaration');
assert.equal(result.directRunReads, true);
assert.equal(result.managedHostReads, true);
assert.equal(result.generated, false);
assert.equal(result.relativePath, 'packages/chat/matrix.json');
});
it('classifies Runtime Host generated state as managed-only generated state', async () => {
const result = await configExplainCommand('/repo/.matrix/home/runtimes/RUNTIME-1/status.json', {
cwd: '/repo',
});
assert.equal(result.kind, 'runtime-host-generated-state');
assert.equal(result.directRunReads, false);
assert.equal(result.managedHostReads, true);
assert.equal(result.generated, true);
});
it('classifies stored API keys as credential state read through resolvers', async () => {
const result = await configExplainCommand(path.join('/repo', '.matrix/home/credentials/matrix-api-key.json'), {
cwd: '/repo',
});
assert.equal(result.kind, 'credential-link-state');
assert.equal(result.directRunReads, true);
assert.equal(result.managedHostReads, true);
assert.match(result.recommendedAction, /never commit/);
});
it('classifies historical deployment paths as deletion candidates', async () => {
const result = await configExplainCommand('packages/dev-platform-deployment/package.json', {
cwd: '/repo',
});
assert.equal(result.kind, 'deletion-candidate');
assert.equal(result.directRunReads, false);
assert.equal(result.managedHostReads, false);
assert.match(result.recommendedAction, /grep\/test proof/);
});
});

View File

@ -0,0 +1,320 @@
import fs from 'node:fs';
import path from 'node:path';
import {
readMatrixCliConfig,
resetMatrixCliConfig,
resolvePackageRegistry,
setMatrixCliConfigValue,
type IMatrixCliConfig,
} from '../utils/config-store.js';
export interface IConfigCommandOptions {
readonly cwd?: string;
readonly configPath?: string;
}
export interface IConfigGetResult {
readonly key: string;
readonly value: string | undefined;
readonly source?: 'explicit' | 'env' | 'config' | 'default';
readonly configPath: string;
}
export interface IConfigListResult {
readonly configPath: string;
readonly values: IMatrixCliConfig;
readonly effective: {
readonly registry: string;
readonly registrySource: 'explicit' | 'env' | 'config' | 'default';
};
}
export interface IConfigSetResult {
readonly key: string;
readonly value: string;
readonly configPath: string;
}
export interface IConfigResetResult {
readonly configPath: string;
readonly values: IMatrixCliConfig;
}
export type ConfigClassificationKind =
| 'direct-actor-input'
| 'package-declaration'
| 'runtime-host-desired-state'
| 'credential-link-state'
| 'runtime-host-generated-state'
| 'runtime-host-preset'
| 'platform-deployment'
| 'deletion-candidate'
| 'unknown';
export interface IConfigExplainResult {
readonly path: string;
readonly relativePath: string;
readonly exists: boolean;
readonly kind: ConfigClassificationKind;
readonly owner: string;
readonly authoredBy: string;
readonly directRunReads: boolean;
readonly managedHostReads: boolean;
readonly generated: boolean;
readonly recommendedAction: string;
readonly notes: readonly string[];
}
function assertSupportedKey(key: string): 'registry' {
const normalized = key.trim();
if (normalized !== 'registry') {
throw new Error(`MX_CONFIG_INVALID: unsupported config key "${key}"`);
}
return normalized;
}
export async function configGetCommand(
key: string,
options: IConfigCommandOptions = {},
): Promise<IConfigGetResult> {
const normalizedKey = assertSupportedKey(key);
const stored = readMatrixCliConfig(options);
const effective = resolvePackageRegistry(undefined, options);
return {
key: normalizedKey,
value: stored.values.registry ?? effective.registry,
source: stored.values.registry ? 'config' : effective.source,
configPath: stored.configPath,
};
}
export async function configListCommand(options: IConfigCommandOptions = {}): Promise<IConfigListResult> {
const stored = readMatrixCliConfig(options);
const effective = resolvePackageRegistry(undefined, options);
return {
configPath: stored.configPath,
values: stored.values,
effective: {
registry: effective.registry,
registrySource: effective.source,
},
};
}
export async function configSetCommand(
key: string,
value: string,
options: IConfigCommandOptions = {},
): Promise<IConfigSetResult> {
const normalizedKey = assertSupportedKey(key);
const result = setMatrixCliConfigValue(normalizedKey, value, options);
return {
key: normalizedKey,
value: result.values.registry ?? '',
configPath: result.configPath,
};
}
export async function configResetCommand(options: IConfigCommandOptions = {}): Promise<IConfigResetResult> {
return resetMatrixCliConfig(options);
}
export async function configExplainCommand(
targetPath: string,
options: IConfigCommandOptions = {},
): Promise<IConfigExplainResult> {
const cwd = options.cwd ?? process.cwd();
const absolutePath = path.resolve(cwd, targetPath);
const relativePath = normalizePath(path.relative(cwd, absolutePath) || path.basename(absolutePath));
const exists = fs.existsSync(absolutePath);
return {
path: absolutePath,
relativePath,
exists,
...classifyConfigPath(relativePath),
};
}
function classifyConfigPath(relativePath: string): Omit<IConfigExplainResult, 'path' | 'relativePath' | 'exists'> {
const normalized = normalizePath(relativePath);
const basename = path.posix.basename(normalized);
const segments = normalized.split('/').filter(Boolean);
if (basename === 'ActorRunnerSmokeActor.mjs' || basename.endsWith('Actor.ts') || basename.endsWith('Actor.mjs')) {
return classification({
kind: 'direct-actor-input',
owner: 'Standalone Actor Runner',
authoredBy: 'CLI caller or package author',
directRunReads: true,
managedHostReads: false,
generated: false,
recommendedAction: 'keep as direct actor input; do not register Host-managed state',
notes: ['Direct actor execution receives this path from the CLI caller.'],
});
}
if (basename === 'matrix.json') {
return classification({
kind: 'package-declaration',
owner: 'Package',
authoredBy: 'package author or scaffold',
directRunReads: true,
managedHostReads: true,
generated: false,
recommendedAction: 'keep; package declarations are shared by direct and managed execution',
notes: [`${basename} is package-authored declaration state.`],
});
}
if (basename === 'package.json.matrix') {
return classification({
kind: 'package-declaration',
owner: 'Package',
authoredBy: 'package author or scaffold',
directRunReads: true,
managedHostReads: true,
generated: false,
recommendedAction: 'migrate to matrix.json when a package-local matrix.json exists',
notes: ['This is a duplicate package manifest candidate, not Runtime Host state.'],
});
}
if (basename === 'host.json' || basename === 'workspace.json' || basename === 'daemon.json') {
return classification({
kind: 'runtime-host-desired-state',
owner: 'Runtime Host',
authoredBy: 'developer, operator, or Host setup tool',
directRunReads: false,
managedHostReads: true,
generated: false,
recommendedAction: 'normalize behind the Runtime Host Plan before changing shape',
notes: ['Direct actor/package execution must not read this file.'],
});
}
if (basename.endsWith('.environment.json') && segments.includes('.matrix')) {
return classification({
kind: 'runtime-host-desired-state',
owner: 'Runtime Host',
authoredBy: 'developer, operator, or package environment author',
directRunReads: false,
managedHostReads: true,
generated: false,
recommendedAction: 'normalize into Runtime Host desired-state model',
notes: ['Package environment manifests belong to managed execution, not direct cloud-bus run.'],
});
}
if (segments.includes('credentials') || basename === 'matrix-api-key.json' || basename === 'hivecast-link.json') {
return classification({
kind: 'credential-link-state',
owner: 'Credentials layer',
authoredBy: 'CLI, cloud exchange, or operator',
directRunReads: true,
managedHostReads: true,
generated: false,
recommendedAction: 'keep secret; never commit; use only through configured credential resolvers',
notes: ['Direct-run may read stored API keys; Runtime Host may read Host/link credentials.'],
});
}
if (
basename === 'host.status.json'
|| basename === 'metadata.webapp'
|| segments.includes('runtimes')
|| segments.includes('runtime-env')
|| segments.includes('package-records')
) {
return classification({
kind: 'runtime-host-generated-state',
owner: 'Runtime Host reconciler',
authoredBy: 'generated by Host runtime',
directRunReads: false,
managedHostReads: true,
generated: true,
recommendedAction: 'keep out of authored config; safe to regenerate from managed Host state',
notes: ['Direct actor/package execution must not create this state.'],
});
}
if (
normalized.includes('/deployments/nodes/')
|| normalized.startsWith('deployments/nodes/')
|| normalized.includes('/dist/profiles/')
|| normalized.startsWith('dist/profiles/')
|| basename.endsWith('.profile.json')
|| basename.endsWith('.node.json')
) {
return classification({
kind: 'runtime-host-preset',
owner: 'Distribution profile resolver',
authoredBy: 'release engineer',
directRunReads: false,
managedHostReads: true,
generated: false,
recommendedAction: 'keep until profile/preset cleanup unifies resolver semantics',
notes: ['Profile and node presets are managed Host inputs, not direct-run inputs.'],
});
}
if (
normalized.includes('/deployments/environments/')
|| normalized.startsWith('deployments/environments/')
|| normalized.includes('/deployments/topologies/')
|| normalized.startsWith('deployments/topologies/')
|| normalized.endsWith('/deployments/schema.json')
|| normalized === 'deployments/schema.json'
|| normalized.includes('/packages/') && normalized.includes('-deployment/')
|| normalized.startsWith('packages/') && normalized.includes('-deployment/')
) {
return classification({
kind: 'deletion-candidate',
owner: 'Historical deployment layer',
authoredBy: 'historical deployment tooling',
directRunReads: false,
managedHostReads: false,
generated: false,
recommendedAction: 'delete only after grep/test proof shows no live reader',
notes: ['This path matches Phase 6 deletion candidates.'],
});
}
if (
basename === 'docker-compose.yml'
|| basename === 'Dockerfile'
|| normalized.includes('/systemd/')
|| normalized.includes('/native-dist/')
) {
return classification({
kind: 'platform-deployment',
owner: 'Platform operator',
authoredBy: 'operator or release engineer',
directRunReads: false,
managedHostReads: false,
generated: false,
recommendedAction: 'treat as deployment packaging, not package/runtime config',
notes: ['Platform deployment files sit outside direct and managed package execution.'],
});
}
return classification({
kind: 'unknown',
owner: 'unknown',
authoredBy: 'unknown',
directRunReads: false,
managedHostReads: false,
generated: false,
recommendedAction: 'inspect before changing; add a classifier case if this becomes a recurring config path',
notes: ['No current config simplification rule matched this path.'],
});
}
function classification(
result: Omit<IConfigExplainResult, 'path' | 'relativePath' | 'exists'>,
): Omit<IConfigExplainResult, 'path' | 'relativePath' | 'exists'> {
return result;
}
function normalizePath(value: string): string {
return value.replace(/\\/g, '/');
}

View File

@ -0,0 +1,43 @@
# @matrix/mx-cli
Matrix CLI for package management.
## Installation
```bash
cd packages/mx-cli
npm install
npm run build
```
## Usage
```bash
mx --help
mx --version
mx init # Initialize a new Matrix package (B013)
mx actor <name> # Scaffold a new actor package (B014)
mx install <package> # Install a package (future)
mx fork <package> # Fork and install a package (future)
mx publish # Publish a package (future)
mx list # List installed packages (future)
```
## Development
- **Build**: `npm run build`
- **Test**: `npm test` (requires Node spawn capability)
## Architecture
See:
- `ARCHITECTURE/protocol/MATRIX-PROTOCOL-10.md` (MXTYPE-1 - Homoiconic Type System)
- `ralph/beads/cycle-0002-B012-mx-cli-package-foundation.md`
- `ralph/beads/cycle-0002-B013-mx-init-command.md`
- `ralph/beads/cycle-0002-B014-mx-actor-scaffold.md`
## Status
- **B012**: Foundation complete (package structure, CLI routing, help/version)
- **B013**: `mx init` implemented (manifest + directory scaffolding + validation)
- **B014**: `mx actor` implemented (actor package scaffold with examples/skills/tests)

View File

@ -0,0 +1,292 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
exchangeActorCredential,
loadActorConstructor,
resolveActorRunProvider,
resolvePackageRunSpec,
} from '../../src/commands/actor-run.js';
describe('matrix actor run', () => {
it('loads a named actor export from an entry module', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-entry-'));
try {
const entry = path.join(dir, 'actors.mjs');
fs.writeFileSync(entry, [
'export class NamedActor {}',
'export class OtherActor {}',
'',
].join('\n'), 'utf8');
const actor = await loadActorConstructor(entry, 'NamedActor');
assert.equal(actor.name, 'NamedActor');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('loads a default actor export from an entry module', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-default-entry-'));
try {
const entry = path.join(dir, 'actor.mjs');
fs.writeFileSync(entry, [
'export default class DefaultActor {}',
'',
].join('\n'), 'utf8');
const actor = await loadActorConstructor(entry, 'default');
assert.equal(actor.name, 'DefaultActor');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('reports available actor exports when the requested export is missing', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-missing-entry-'));
try {
const entry = path.join(dir, 'actor.mjs');
fs.writeFileSync(entry, [
'export class PresentActor {}',
'',
].join('\n'), 'utf8');
await assert.rejects(
() => loadActorConstructor(entry, 'MissingActor'),
/No actor class found as export "MissingActor".*PresentActor/u,
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('resolves API-key, cloud, and Space from an explicit home without Host state', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-'));
try {
fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
fs.writeFileSync(
path.join(home, 'credentials', 'matrix-api-key.json'),
`${JSON.stringify({
apiKey: 'hc_home_key',
cloud: 'https://dev.hivecast.ai/',
authorityRoot: 'richard-santomauro-nimbletec-com',
})}\n`,
'utf8',
);
const resolved = resolveActorRunProvider({ home });
assert.equal(resolved.key, 'hc_home_key');
assert.equal(resolved.cloud, 'https://dev.hivecast.ai');
assert.equal(resolved.space, 'richard-santomauro-nimbletec-com');
assert.ok(resolved.checked.includes('<home>/credentials/matrix-api-key.json'));
assert.equal(fs.existsSync(path.join(home, 'host.json')), false);
assert.equal(fs.existsSync(path.join(home, 'runtimes')), false);
assert.equal(fs.existsSync(path.join(home, 'package-records')), false);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
it('prefers explicit provider options over environment and home configuration', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-precedence-'));
const previous = {
MATRIX_API_KEY: process.env.MATRIX_API_KEY,
HIVECAST_API_KEY: process.env.HIVECAST_API_KEY,
MATRIX_CLOUD: process.env.MATRIX_CLOUD,
HIVECAST_CLOUD: process.env.HIVECAST_CLOUD,
MATRIX_SPACE: process.env.MATRIX_SPACE,
HIVECAST_SPACE: process.env.HIVECAST_SPACE,
};
try {
fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
fs.writeFileSync(
path.join(home, 'credentials', 'matrix-api-key.json'),
`${JSON.stringify({
apiKey: 'hc_home_key',
cloud: 'https://home.hivecast.ai',
space: 'home-space',
})}\n`,
'utf8',
);
process.env.MATRIX_API_KEY = 'hc_env_key';
process.env.MATRIX_CLOUD = 'https://env.hivecast.ai';
process.env.MATRIX_SPACE = 'env-space';
const resolved = resolveActorRunProvider({
home,
key: 'hc_explicit_key',
cloud: 'https://explicit.hivecast.ai/',
space: 'explicit-space',
});
assert.equal(resolved.key, 'hc_explicit_key');
assert.equal(resolved.cloud, 'https://explicit.hivecast.ai');
assert.equal(resolved.space, 'explicit-space');
} finally {
restoreEnv(previous);
fs.rmSync(home, { recursive: true, force: true });
}
});
it('reports missing key, cloud, and Space before contacting the cloud', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-missing-'));
const previous = {
MATRIX_API_KEY: process.env.MATRIX_API_KEY,
HIVECAST_API_KEY: process.env.HIVECAST_API_KEY,
MATRIX_CLOUD: process.env.MATRIX_CLOUD,
HIVECAST_CLOUD: process.env.HIVECAST_CLOUD,
MATRIX_SPACE: process.env.MATRIX_SPACE,
HIVECAST_SPACE: process.env.HIVECAST_SPACE,
};
try {
delete process.env.MATRIX_API_KEY;
delete process.env.HIVECAST_API_KEY;
delete process.env.MATRIX_CLOUD;
delete process.env.HIVECAST_CLOUD;
delete process.env.MATRIX_SPACE;
delete process.env.HIVECAST_SPACE;
assert.throws(
() => resolveActorRunProvider({ home }),
/MATRIX_API_KEY_REQUIRED/u,
);
assert.throws(
() => resolveActorRunProvider({ home, key: 'hc_key' }),
/MATRIX_CLOUD_REQUIRED/u,
);
assert.throws(
() => resolveActorRunProvider({ home, key: 'hc_key', cloud: 'https://dev.hivecast.ai' }),
/MATRIX_SPACE_REQUIRED/u,
);
} finally {
restoreEnv(previous);
fs.rmSync(home, { recursive: true, force: true });
}
});
it('exchanges through /api/install/actor-credential without putting the key in the URL', async () => {
const requests: Array<{ readonly url: string; readonly init: Record<string, unknown> }> = [];
const provider = {
key: 'hc_actor_key',
cloud: 'https://dev.hivecast.ai',
space: 'richard-santomauro-nimbletec-com',
checked: [],
};
const fetchImpl = async (url: URL, init: RequestInit): Promise<Response> => {
requests.push({ url: url.toString(), init: init as Record<string, unknown> });
return new Response(JSON.stringify({
ok: true,
space: { authorityRoot: 'richard-santomauro-nimbletec-com' },
transport: { natsUrl: 'nats://dev.hivecast.ai:4422' },
mount: 'apps.ping',
effectiveMount: 'richard-santomauro-nimbletec-com.apps.ping',
credentials: { jwt: 'jwt', seed: 'SU' },
source: 'standalone-actor',
credentialPipeline: 'api-key-exchange',
}), {
status: 200,
headers: { 'content-type': 'application/json' },
});
};
const exchanged = await exchangeActorCredential({
provider,
mount: 'apps.ping',
actorId: 'PingActor',
packageId: '@acme/ping',
runtimeId: 'standalone-actor:ping',
accepts: { ping: {} },
emits: {},
subscribes: {},
ttlSeconds: 90,
fetchImpl,
});
assert.equal(exchanged.ok, true);
assert.equal(requests.length, 1);
assert.equal(requests[0]?.url, 'https://dev.hivecast.ai/api/install/actor-credential');
assert.equal(JSON.stringify(requests[0]).includes('hc_actor_key'), true);
assert.equal(requests[0]?.url.includes('hc_actor_key'), false);
assert.equal((requests[0]?.init.headers as Record<string, string>).authorization, 'Bearer hc_actor_key');
const body = JSON.parse(String(requests[0]?.init.body)) as Record<string, unknown>;
assert.equal(body.space, 'richard-santomauro-nimbletec-com');
assert.equal(body.mount, 'apps.ping');
assert.equal(body.actorId, 'PingActor');
assert.deepEqual(body.accepts, { ping: {} });
assert.equal(body.ttlSeconds, 90);
});
it('resolves direct package run metadata from matrix.json runtime.factory without Host state', () => {
const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-package-run-spec-'));
try {
fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'package.json'),
`${JSON.stringify({ name: '@example/direct-run', version: '0.1.0', type: 'module' }, null, 2)}\n`,
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
`${JSON.stringify({
name: '@example/direct-run',
version: '0.1.0',
runtime: {
language: 'javascript',
entry: 'src/index.mjs',
factory: {
kind: 'factory',
export: 'createDirectRunActor',
instance: {
id: 'RUNTIME-HOST-DIRECT-RUN',
class: 'service',
rootKind: 'component',
componentType: 'DirectRunActor',
mount: 'actor-runner-smoke',
autoStart: true,
},
},
},
components: [{
type: 'DirectRunActor',
mount: 'actor-runner-smoke',
accepts: ['ping', 'getStatus'],
}],
}, null, 2)}\n`,
'utf8',
);
fs.writeFileSync(path.join(packageDir, 'src', 'index.mjs'), 'export function createDirectRunActor() {}\n', 'utf8');
const spec = resolvePackageRunSpec(packageDir, { mount: 'actor-runner-smoke' });
assert.equal(spec.packageName, '@example/direct-run');
assert.equal(spec.manifestPath, path.join(packageDir, 'matrix.json'));
assert.equal(spec.exportName, 'createDirectRunActor');
assert.equal(spec.mount, 'actor-runner-smoke');
assert.equal(spec.actorId, 'DirectRunActor');
assert.equal(spec.packageId, '@example/direct-run');
assert.equal(spec.accepts.ping && typeof spec.accepts.ping, 'object');
assert.equal(spec.accepts.getStatus && typeof spec.accepts.getStatus, 'object');
assert.equal(fs.existsSync(path.join(packageDir, 'host.json')), false);
assert.equal(fs.existsSync(path.join(packageDir, 'runtimes')), false);
assert.equal(fs.existsSync(path.join(packageDir, 'package-records')), false);
} finally {
fs.rmSync(packageDir, { recursive: true, force: true });
}
});
});
function restoreEnv(previous: Record<string, string | undefined>): void {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}

View File

@ -0,0 +1,693 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.js';
import { NatsTransport } from '@open-matrix/core/transport/NatsTransport.js';
import { connect as natsConnect, jwtAuthenticator, type NatsConnection } from 'nats';
import { loadMatrixServiceManifest, type ILoadedMatrixServiceManifest } from '../utils/service-manifest.js';
import { createRunnerSecurityRealm } from '../utils/runner-security.js';
export interface IActorRunCommandOptions {
readonly key?: string;
readonly cloud?: string;
readonly space?: string;
readonly home?: string;
readonly export?: string;
readonly mount?: string;
readonly json?: boolean;
readonly props?: string;
readonly ttlSeconds?: number;
readonly checkOp?: string;
readonly checkPayload?: string;
}
export interface IActorRunProviderResolution {
readonly key: string;
readonly cloud: string;
readonly space: string;
readonly checked: readonly string[];
}
interface IActorCredentialExchange {
readonly ok?: unknown;
readonly error?: unknown;
readonly space?: {
readonly authorityRoot?: unknown;
readonly spaceId?: unknown;
readonly routeKey?: unknown;
readonly publicNamespace?: unknown;
};
readonly transport?: {
readonly natsUrl?: unknown;
readonly root?: unknown;
};
readonly mount?: unknown;
readonly effectiveMount?: unknown;
readonly credentials?: {
readonly jwt?: unknown;
readonly seed?: unknown;
readonly userPublicKey?: unknown;
readonly expiresAt?: unknown;
};
readonly source?: unknown;
readonly credentialPipeline?: unknown;
}
type ActorConstructor = {
new (): MatrixActor;
readonly accepts?: Record<string, unknown>;
readonly emits?: Record<string, unknown>;
readonly subscribes?: Record<string, unknown>;
readonly name: string;
};
type PackageBootstrapFunction = (...args: readonly unknown[]) => Promise<unknown> | unknown;
export interface IPackageRunCommandOptions extends IActorRunCommandOptions {
readonly entry?: string;
readonly overrides?: string;
}
export interface IPackageRunSpec {
readonly packageDir: string;
readonly packageName: string;
readonly manifestPath: string;
readonly entryPath: string;
readonly exportName: string;
readonly mount: string;
readonly actorId: string;
readonly packageId: string;
readonly runtimeId: string;
readonly accepts: Record<string, unknown>;
readonly emits: Record<string, unknown>;
readonly subscribes: Record<string, unknown>;
readonly overrides: Record<string, unknown>;
readonly serviceInstance: ILoadedMatrixServiceManifest['serviceInstance'];
}
export async function actorRunCommand(
entry: string,
options: IActorRunCommandOptions,
): Promise<void> {
const entryPath = path.resolve(entry);
if (!fs.existsSync(entryPath)) {
throw new Error(`Actor entry not found: ${entryPath}`);
}
const exportName = options.export?.trim() || 'default';
const ActorClass = await loadActorConstructor(entryPath, exportName);
const mount = options.mount?.trim() || inferMountFromEntry(entryPath);
if (!mount) {
throw new Error('matrix actor run requires --mount <mount> when it cannot infer a mount from the entry file');
}
const provider = resolveActorRunProvider(options);
const exchange = await exchangeActorCredential({
provider,
mount,
actorId: ActorClass.name || mount,
packageId: readPackageNameForEntry(entryPath) ?? 'standalone-actor',
runtimeId: `standalone-actor:${process.pid}:${mount}`,
accepts: ActorClass.accepts ?? {},
emits: ActorClass.emits ?? {},
subscribes: ActorClass.subscribes ?? {},
ttlSeconds: options.ttlSeconds,
});
const authorityRoot = readRequiredString(exchange.space?.authorityRoot, 'actor credential response space.authorityRoot');
const natsUrl = readRequiredString(exchange.transport?.natsUrl, 'actor credential response transport.natsUrl');
const jwt = readRequiredString(exchange.credentials?.jwt, 'actor credential response credentials.jwt');
const seed = readRequiredString(exchange.credentials?.seed, 'actor credential response credentials.seed');
const connection = await connectActorNats({
natsUrl,
authorityRoot,
jwt,
seed,
});
const transport = new NatsTransport(connection, { root: authorityRoot });
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
});
const props = parseProps(options.props);
await runtime.createSupervised(ActorClass, mount, props);
await transport.flush();
const check = await runOptionalActorCheck(runtime, mount, options);
const result = {
ok: true,
mode: 'standalone-actor-runner',
entry: entryPath,
export: exportName,
actor: ActorClass.name,
mount,
authorityRoot,
effectiveMount: resolveEffectiveMount(exchange.effectiveMount, authorityRoot, mount),
cloud: provider.cloud,
natsUrl,
source: readRequiredString(exchange.source, 'actor credential response source'),
credentialPipeline: readRequiredString(exchange.credentialPipeline, 'actor credential response credentialPipeline'),
hostStateWritten: false,
...(check ? { check } : {}),
};
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log('Actor mounted:');
console.log(` logical: ${result.mount}`);
console.log(` authority: ${result.authorityRoot}`);
console.log(` effective: ${result.effectiveMount}`);
console.log(` cloud: ${result.cloud}`);
console.log('Press Ctrl+C to stop.');
}
if (check) {
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
return;
}
await waitForActorShutdown(async () => {
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
});
}
export async function packageRunCommand(
packageDir: string,
options: IPackageRunCommandOptions,
): Promise<void> {
const spec = resolvePackageRunSpec(packageDir, options);
const provider = resolveActorRunProvider(options);
const exchange = await exchangeActorCredential({
provider,
mount: spec.mount,
actorId: spec.actorId,
packageId: spec.packageId,
runtimeId: spec.runtimeId,
accepts: spec.accepts,
emits: spec.emits,
subscribes: spec.subscribes,
ttlSeconds: options.ttlSeconds,
});
const authorityRoot = readRequiredString(exchange.space?.authorityRoot, 'actor credential response space.authorityRoot');
const natsUrl = readRequiredString(exchange.transport?.natsUrl, 'actor credential response transport.natsUrl');
const jwt = readRequiredString(exchange.credentials?.jwt, 'actor credential response credentials.jwt');
const seed = readRequiredString(exchange.credentials?.seed, 'actor credential response credentials.seed');
const connection = await connectActorNats({
natsUrl,
authorityRoot,
jwt,
seed,
});
const transport = new NatsTransport(connection, { root: authorityRoot });
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
});
const bootstrap = await loadPackageBootstrap(spec.entryPath, spec.exportName);
const bootstrapResult = await bootstrap(
spec.overrides,
{ runtime },
undefined,
{
packageName: spec.packageName,
packageDir: spec.packageDir,
manifestPath: spec.manifestPath,
entryPath: spec.entryPath,
exportName: spec.exportName,
root: authorityRoot,
mount: spec.mount,
instance: spec.serviceInstance,
serviceInstance: spec.serviceInstance,
},
);
await transport.flush();
const check = await runOptionalActorCheck(runtime, spec.mount, options);
const result = {
ok: true,
mode: 'standalone-package-runner',
packageDir: spec.packageDir,
packageName: spec.packageName,
manifest: spec.manifestPath,
entry: spec.entryPath,
export: spec.exportName,
mount: spec.mount,
authorityRoot,
effectiveMount: resolveEffectiveMount(exchange.effectiveMount, authorityRoot, spec.mount),
cloud: provider.cloud,
natsUrl,
source: readRequiredString(exchange.source, 'actor credential response source'),
credentialPipeline: readRequiredString(exchange.credentialPipeline, 'actor credential response credentialPipeline'),
hostStateWritten: false,
...(check ? { check } : {}),
};
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log('Package actor mounted:');
console.log(` package: ${result.packageName}`);
console.log(` logical: ${result.mount}`);
console.log(` authority: ${result.authorityRoot}`);
console.log(` effective: ${result.effectiveMount}`);
console.log(` cloud: ${result.cloud}`);
console.log('Press Ctrl+C to stop.');
}
if (check) {
await shutdownPackageBootstrapResult(bootstrapResult, runtime);
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
return;
}
await waitForActorShutdown(async () => {
await shutdownPackageBootstrapResult(bootstrapResult, runtime);
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
});
}
export function resolveActorRunProvider(options: IActorRunCommandOptions): IActorRunProviderResolution {
const home = path.resolve(options.home?.trim() || path.join(os.homedir(), '.matrix'));
const homeCredential = readApiKeyCredentialFile(path.join(home, 'credentials', 'matrix-api-key.json'));
const homeConfig = readApiKeyConfigFile(path.join(home, 'config.json'));
const defaultHome = path.resolve(os.homedir(), '.matrix');
const defaultCredential = home === defaultHome ? {} : readApiKeyCredentialFile(path.join(defaultHome, 'credentials', 'matrix-api-key.json'));
const defaultConfig = home === defaultHome ? {} : readApiKeyConfigFile(path.join(defaultHome, 'config.json'));
const hivecastNamedCredential = readApiKeyCredentialFile(path.join(defaultHome, 'credentials', 'hivecast-api-key.json'));
const key = readOptionalString(options.key)
?? readOptionalString(process.env.MATRIX_API_KEY)
?? readOptionalString(process.env.HIVECAST_API_KEY)
?? homeCredential.key
?? homeConfig.key
?? defaultCredential.key
?? defaultConfig.key
?? hivecastNamedCredential.key;
const cloud = readOptionalString(options.cloud)
?? readOptionalString(process.env.MATRIX_CLOUD)
?? readOptionalString(process.env.HIVECAST_CLOUD)
?? homeCredential.cloud
?? homeConfig.cloud
?? defaultCredential.cloud
?? defaultConfig.cloud
?? hivecastNamedCredential.cloud;
const space = readOptionalString(options.space)
?? readOptionalString(process.env.MATRIX_SPACE)
?? readOptionalString(process.env.HIVECAST_SPACE)
?? homeCredential.space
?? homeConfig.space
?? defaultCredential.space
?? defaultConfig.space
?? hivecastNamedCredential.space;
const checked = [
'--key',
'MATRIX_API_KEY',
'HIVECAST_API_KEY',
'<home>/credentials/matrix-api-key.json',
'<home>/config.json',
'~/.matrix/credentials/matrix-api-key.json',
'~/.matrix/config.json',
'~/.matrix/credentials/hivecast-api-key.json',
];
if (!key) {
throw new Error(`MATRIX_API_KEY_REQUIRED: matrix actor run requires --key or a configured API key. Checked: ${checked.join(', ')}`);
}
if (!cloud) {
throw new Error('MATRIX_CLOUD_REQUIRED: matrix actor run requires --cloud or configured MATRIX_CLOUD/HIVECAST_CLOUD');
}
if (!space) {
throw new Error('MATRIX_SPACE_REQUIRED: matrix actor run requires --space or configured MATRIX_SPACE/HIVECAST_SPACE');
}
return {
key,
cloud: normalizeCloudUrl(cloud),
space,
checked,
};
}
export async function exchangeActorCredential(input: {
readonly provider: IActorRunProviderResolution;
readonly mount: string;
readonly actorId: string;
readonly packageId: string;
readonly runtimeId: string;
readonly accepts: Record<string, unknown>;
readonly emits: Record<string, unknown>;
readonly subscribes: Record<string, unknown>;
readonly ttlSeconds?: number;
readonly fetchImpl?: typeof fetch;
}): Promise<IActorCredentialExchange> {
const fetchImpl = input.fetchImpl ?? fetch;
const response = await fetchImpl(new URL('/api/install/actor-credential', input.provider.cloud), {
method: 'POST',
headers: {
authorization: `Bearer ${input.provider.key}`,
'content-type': 'application/json',
},
body: JSON.stringify({
space: input.provider.space,
mount: input.mount,
actorId: input.actorId,
packageId: input.packageId,
runtimeId: input.runtimeId,
accepts: input.accepts,
emits: input.emits,
subscribes: input.subscribes,
...(input.ttlSeconds ? { ttlSeconds: input.ttlSeconds } : {}),
}),
});
const body = await response.json().catch(() => null) as IActorCredentialExchange | null;
if (!response.ok || body?.ok !== true) {
const error = readOptionalString(body?.error) ?? `HTTP_${response.status}`;
throw new Error(`ACTOR_CREDENTIAL_EXCHANGE_FAILED: ${error}`);
}
return body;
}
export function resolvePackageRunSpec(packageDir: string, options: IPackageRunCommandOptions): IPackageRunSpec {
const loaded = loadMatrixServiceManifest(packageDir);
const entryPath = options.entry
? resolvePackageEntryPath(loaded.packageDir, options.entry)
: loaded.entryPath;
const exportName = options.export?.trim() || loaded.exportName;
const mount = options.mount?.trim() || loaded.mount;
if (!mount) {
throw new Error('matrix package run requires --mount <mount> when the package descriptor does not declare one');
}
const binding = resolvePackageBindingForMount(loaded, mount);
const componentType = binding?.type ?? loaded.serviceInstance.componentType ?? loaded.serviceInstance.class;
const actorId = componentType || mount;
const runtimeId = `standalone-package:${process.pid}:${sanitizeRuntimeIdPart(loaded.packageName)}:${sanitizeRuntimeIdPart(mount)}`;
return {
packageDir: loaded.packageDir,
packageName: loaded.packageName,
manifestPath: loaded.manifestPath,
entryPath,
exportName,
mount,
actorId,
packageId: loaded.packageName,
runtimeId,
accepts: acceptsArrayToRecord(binding?.accepts ?? []),
emits: {},
subscribes: {},
overrides: parseOverrides(options.overrides),
serviceInstance: loaded.serviceInstance,
};
}
async function connectActorNats(input: {
readonly natsUrl: string;
readonly authorityRoot: string;
readonly jwt: string;
readonly seed: string;
}): Promise<NatsConnection> {
const encoder = new TextEncoder();
return await natsConnect({
servers: [input.natsUrl],
name: `matrix-actor-run-${input.authorityRoot}-${process.pid}`,
timeout: 5_000,
authenticator: jwtAuthenticator(
() => input.jwt,
() => encoder.encode(input.seed),
),
});
}
export async function loadActorConstructor(entryPath: string, exportName: string): Promise<ActorConstructor> {
const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_actor_run=${Date.now()}-${Math.random().toString(16).slice(2)}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const requested = actorConstructorFromExport(mod[exportName]);
if (requested) {
return requested;
}
const available = Object.entries(mod)
.filter(([, value]) => actorConstructorFromExport(value))
.map(([name]) => name);
throw new Error(`No actor class found as export "${exportName}". Available exports: ${available.join(', ') || '(none)'}`);
}
async function loadPackageBootstrap(entryPath: string, exportName: string): Promise<PackageBootstrapFunction> {
const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_package_run=${Date.now()}-${Math.random().toString(16).slice(2)}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const bootstrap = mod[exportName];
if (typeof bootstrap !== 'function') {
throw new Error(`Package run export "${exportName}" was not found in ${entryPath}`);
}
return bootstrap as PackageBootstrapFunction;
}
function actorConstructorFromExport(value: unknown): ActorConstructor | null {
if (typeof value !== 'function') {
return null;
}
const candidate = value as ActorConstructor;
return candidate.prototype ? candidate : null;
}
function inferMountFromEntry(entryPath: string): string {
return path.basename(entryPath, path.extname(entryPath))
.replace(/Actor$/i, '')
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function readPackageNameForEntry(entryPath: string): string | undefined {
let dir = path.dirname(entryPath);
while (dir !== path.dirname(dir)) {
const packageJsonPath = path.join(dir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const parsed = readJsonFile(packageJsonPath);
return readOptionalString(parsed?.name);
}
dir = path.dirname(dir);
}
return undefined;
}
function parseProps(value: string | undefined): Record<string, unknown> | undefined {
const text = value?.trim();
if (!text) {
return undefined;
}
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('matrix actor run --props must be a JSON object');
}
return parsed as Record<string, unknown>;
}
function parseOverrides(value: string | undefined): Record<string, unknown> {
const text = value?.trim();
if (!text) {
return {};
}
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('matrix package run --overrides must be a JSON object');
}
return parsed as Record<string, unknown>;
}
async function runOptionalActorCheck(
runtime: MatrixRuntime,
mount: string,
options: IActorRunCommandOptions,
): Promise<{ readonly op: string; readonly result: unknown } | null> {
const op = options.checkOp?.trim();
if (!op) {
return null;
}
const payload = parseCheckPayload(options.checkPayload);
const result = await RequestReply.execute<Record<string, unknown>, unknown>(
runtime.getRootContext(),
mount,
op,
payload,
{ timeoutMs: 5_000 },
);
return { op, result };
}
function parseCheckPayload(value: string | undefined): Record<string, unknown> {
const text = value?.trim();
if (!text) {
return {};
}
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('matrix actor/package run --check-payload must be a JSON object');
}
return parsed as Record<string, unknown>;
}
function resolvePackageEntryPath(packageDir: string, entry: string): string {
const trimmed = entry.trim();
if (!trimmed) {
throw new Error('matrix package run --entry must be a non-empty path');
}
const entryPath = path.resolve(packageDir, trimmed);
if (!fs.existsSync(entryPath)) {
throw new Error(`matrix package run entry does not exist: ${trimmed}`);
}
return entryPath;
}
function resolvePackageBindingForMount(
loaded: ILoadedMatrixServiceManifest,
mount: string,
): ILoadedMatrixServiceManifest['packageComponents'][number] | ILoadedMatrixServiceManifest['packageRoot'] | undefined {
if (loaded.packageRoot?.mount === mount) {
return loaded.packageRoot;
}
return loaded.packageComponents.find((entry) => entry.mount === mount);
}
function acceptsArrayToRecord(accepts: readonly string[]): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const op of accepts) {
result[op] = {};
}
return result;
}
function sanitizeRuntimeIdPart(value: string): string {
return value
.replace(/^@/u, '')
.replace(/[^a-zA-Z0-9._-]+/gu, '-')
.replace(/-+/gu, '-')
.replace(/^-|-$/gu, '')
|| 'package';
}
async function shutdownPackageBootstrapResult(value: unknown, ownedRuntime: MatrixRuntime): Promise<void> {
if (!value || typeof value !== 'object') {
return;
}
const runtime = (value as { readonly runtime?: unknown }).runtime;
if (!runtime || typeof runtime !== 'object') {
return;
}
if (runtime === ownedRuntime) {
return;
}
const shutdown = (runtime as { readonly shutdown?: unknown }).shutdown;
if (typeof shutdown === 'function') {
await shutdown.call(runtime);
}
}
function waitForActorShutdown(onShutdown: () => Promise<void>): Promise<void> {
return new Promise<void>((resolve, reject) => {
let settled = false;
const finish = (error?: unknown): void => {
if (settled) return;
settled = true;
process.off('SIGINT', shutdown);
process.off('SIGTERM', shutdown);
if (error) {
reject(error);
return;
}
resolve();
};
const shutdown = (): void => {
void onShutdown().then(() => finish()).catch((error) => finish(error));
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
});
}
function readApiKeyCredentialFile(filePath: string): { readonly key?: string; readonly cloud?: string; readonly space?: string } {
const value = readJsonFile(filePath);
return {
...(readOptionalString(value?.apiKey) ?? readOptionalString(value?.key) ? { key: (readOptionalString(value?.apiKey) ?? readOptionalString(value?.key))! } : {}),
...(readOptionalString(value?.cloud) ? { cloud: readOptionalString(value?.cloud)! } : {}),
...(readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot) ? { space: (readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot))! } : {}),
};
}
function readApiKeyConfigFile(filePath: string): { readonly key?: string; readonly cloud?: string; readonly space?: string } {
const value = readJsonFile(filePath);
const apiKey = value?.apiKey && typeof value.apiKey === 'object' && !Array.isArray(value.apiKey)
? value.apiKey as Record<string, unknown>
: {};
return {
...(readOptionalString(apiKey.apiKey) ?? readOptionalString(apiKey.key) ? { key: (readOptionalString(apiKey.apiKey) ?? readOptionalString(apiKey.key))! } : {}),
...(readOptionalString(value?.cloud) ?? readOptionalString(value?.hivecastCloud) ? { cloud: (readOptionalString(value?.cloud) ?? readOptionalString(value?.hivecastCloud))! } : {}),
...(readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot) ? { space: (readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot))! } : {}),
};
}
function readJsonFile(filePath: string): Record<string, unknown> | null {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid JSON in ${filePath}: ${message}`);
}
}
function readRequiredString(value: unknown, label: string): string {
const result = readOptionalString(value);
if (!result) {
throw new Error(`Missing ${label}`);
}
return result;
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function resolveEffectiveMount(value: unknown, authorityRoot: string, mount: string): string {
const fromExchange = readOptionalString(value);
if (fromExchange) {
return fromExchange;
}
return `${authorityRoot}.${mount}`;
}
function normalizeCloudUrl(value: string): string {
const parsed = new URL(value);
return parsed.toString().replace(/\/$/, '');
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,305 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
decideMatrixPlacementBind,
type AuthorityCoordinatorLease,
type MatrixMountCardinality,
type MatrixPlacementBindDecision,
type MatrixPlacementBindRequest,
type MatrixPlacementPolicy,
type RuntimeInventoryPlacementMetadata,
} from '@open-matrix/contracts/placement';
import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import type { ILoadedMatrixPackageEnvironment } from './package-environment.js';
import type { ILoadedMatrixServiceManifest } from './service-manifest.js';
export const MATRIX_PLACEMENT_POLICY_SERVICE = 'matrix-placement-policy';
export const MATRIX_PLACEMENT_STANDBY_RESULTS_SERVICE = 'matrix-placement-standby-results';
export interface RunnerPlacementStandbyResult {
readonly mount: string;
readonly runtimeId: string;
readonly packageName: string;
readonly placement: RuntimeInventoryPlacementMetadata;
}
interface PlacementDeclaration {
readonly cardinality: MatrixMountCardinality;
readonly physicalMount?: string;
}
interface NodeTopologySnapshot {
readonly kind?: string;
readonly nodeId?: string;
readonly authorityRoot: string;
readonly authorityCoordinatorActive: boolean;
readonly authorityLease?: AuthorityCoordinatorLease | null;
}
type SupervisedCreate = MatrixRuntime['createSupervised'];
export function installRunnerPlacementPolicy(options: {
readonly runtime: MatrixRuntime;
readonly manifest: ILoadedMatrixServiceManifest;
readonly environment: ILoadedMatrixPackageEnvironment | undefined;
readonly runtimeId: string;
readonly authorityRoot: string | undefined;
}): void {
const declarations = readPlacementDeclarations(options.manifest);
const topology = resolveTopologySnapshot(options.runtime, options.environment, options.authorityRoot);
const standbyResults: RunnerPlacementStandbyResult[] = [];
const policy: MatrixPlacementPolicy = {
canBind(input: MatrixPlacementBindRequest): MatrixPlacementBindDecision {
return decideMatrixPlacementBind(input);
},
};
options.runtime.registerService(MATRIX_PLACEMENT_POLICY_SERVICE, policy);
options.runtime.registerService(MATRIX_PLACEMENT_STANDBY_RESULTS_SERVICE, standbyResults);
const originalCreateSupervised = options.runtime.createSupervised.bind(options.runtime) as SupervisedCreate;
options.runtime.createSupervised = (async <T extends MatrixActor>(
ComponentClass: new () => T,
mount: string,
props?: Record<string, unknown>,
): Promise<T> => {
const trimmedMount = mount.trim();
const declaration = declarations.get(trimmedMount);
if (!declaration) {
return await originalCreateSupervised(ComponentClass, mount, props);
}
const physicalMount = declaration.physicalMount ?? trimmedMount;
const request: MatrixPlacementBindRequest = {
authorityRoot: topology.authorityRoot,
runtimeId: options.runtimeId,
packageName: options.manifest.packageName,
mount: trimmedMount,
cardinality: declaration.cardinality,
...(topology.kind ? { topologyKind: topology.kind } : {}),
authorityCoordinatorActive: topology.authorityCoordinatorActive,
...(topology.nodeId ? { ownerNodeId: topology.nodeId } : {}),
...(topology.authorityLease !== undefined ? { authorityLease: topology.authorityLease } : {}),
};
const decision = policy.canBind(request);
if (decision.allowed) {
return await originalCreateSupervised(ComponentClass, mount, props);
}
standbyResults.push({
mount: trimmedMount,
runtimeId: options.runtimeId,
packageName: options.manifest.packageName,
placement: {
cardinality: declaration.cardinality,
claimMode: claimModeForCardinality(declaration.cardinality),
logicalMount: trimmedMount,
physicalMount,
callable: decision.callable,
...(decision.reason ? { standbyReason: decision.reason } : {}),
},
});
throw new Error(`Placement denied for ${trimmedMount}: ${decision.reason ?? 'UNKNOWN_PLACEMENT'}`);
}) as SupervisedCreate;
}
export function readRunnerPlacementStandbyResults(runtime: MatrixRuntime): readonly RunnerPlacementStandbyResult[] {
return runtime.getRootContext().getService<readonly RunnerPlacementStandbyResult[]>(
MATRIX_PLACEMENT_STANDBY_RESULTS_SERVICE,
) ?? [];
}
function resolveTopologySnapshot(
runtime: MatrixRuntime,
environment: ILoadedMatrixPackageEnvironment | undefined,
authorityRoot: string | undefined,
): NodeTopologySnapshot {
const currentRoot = authorityRoot?.trim()
|| environment?.environment.runtime.root?.trim()
|| runtime.transport.root.trim();
const topology = readTopologyRecord(runtime.getRootContext().getService<unknown>('matrix-node-topology'))
?? readTopologyRecord(environment?.environment.topology);
const topologyAuthorityRoot = readAuthorityRoot(topology) ?? currentRoot;
const active = readAuthorityCoordinatorActive(topology);
const authorityLease = readAuthorityLease(runtime, environment, topology);
return {
...(readString(topology?.kind) ? { kind: readString(topology?.kind) } : {}),
...(readString(topology?.nodeId) ? { nodeId: readString(topology?.nodeId) } : {}),
authorityRoot: topologyAuthorityRoot,
authorityCoordinatorActive: active ?? true,
...(authorityLease !== undefined ? { authorityLease } : {}),
};
}
function readAuthorityLease(
runtime: MatrixRuntime,
environment: ILoadedMatrixPackageEnvironment | undefined,
topology: Record<string, unknown> | undefined,
): AuthorityCoordinatorLease | null | undefined {
const serviceLease = readAuthorityLeaseRecord(runtime.getRootContext().getService<unknown>('matrix-authority-coordinator-lease'));
if (serviceLease !== undefined) {
return serviceLease;
}
const topologyLease = readAuthorityLeaseRecord(topology?.authorityLease);
if (topologyLease !== undefined) {
return topologyLease;
}
return readAuthorityLeaseRecord(environment?.environment.authorityLease);
}
function readPlacementDeclarations(manifest: ILoadedMatrixServiceManifest): ReadonlyMap<string, PlacementDeclaration> {
const declarations = new Map<string, PlacementDeclaration>();
const manifestRecord = readJsonObjectIfPresent(path.join(manifest.packageDir, 'matrix.json'));
if (!manifestRecord) {
return declarations;
}
addPlacementDeclaration(declarations, manifestRecord.root);
const components = manifestRecord.components;
if (Array.isArray(components)) {
for (const component of components) {
addPlacementDeclaration(declarations, component);
}
}
return declarations;
}
function addPlacementDeclaration(declarations: Map<string, PlacementDeclaration>, value: unknown): void {
if (!isRecord(value)) {
return;
}
const mount = readString(value.mount);
const placement = isRecord(value.placement) ? value.placement : undefined;
const cardinality = readMountCardinality(placement?.cardinality);
if (!mount || !cardinality) {
return;
}
const physicalMount = readString(placement?.physicalMount);
declarations.set(mount, {
cardinality,
...(physicalMount ? { physicalMount } : {}),
});
}
function readTopologyRecord(value: unknown): Record<string, unknown> | undefined {
return isRecord(value) ? value : undefined;
}
function readAuthorityRoot(topology: Record<string, unknown> | undefined): string | undefined {
const authority = isRecord(topology?.authority) ? topology.authority : undefined;
return readString(authority?.authorityRoot);
}
function readAuthorityCoordinatorActive(topology: Record<string, unknown> | undefined): boolean | undefined {
const roles = isRecord(topology?.roles) ? topology.roles : undefined;
const authorityCoordinator = isRecord(roles?.authorityCoordinator) ? roles.authorityCoordinator : undefined;
return typeof authorityCoordinator?.active === 'boolean' ? authorityCoordinator.active : undefined;
}
function readAuthorityLeaseRecord(value: unknown): AuthorityCoordinatorLease | null | undefined {
if (value === null) {
return null;
}
if (!isRecord(value)) {
return undefined;
}
if (
value.version !== 1
|| typeof value.authorityRoot !== 'string'
|| typeof value.leaseId !== 'string'
|| typeof value.ownerNodeId !== 'string'
|| typeof value.placementEpoch !== 'number'
|| typeof value.acquiredAt !== 'string'
|| typeof value.heartbeatAt !== 'string'
|| typeof value.expiresAt !== 'string'
) {
return undefined;
}
if (
value.status !== 'active'
&& value.status !== 'released'
&& value.status !== 'expired'
&& value.status !== 'fenced'
) {
return undefined;
}
if (
value.scope !== 'local-home'
&& value.scope !== 'hub-authority'
&& value.scope !== 'remote-projection'
) {
return undefined;
}
if (
value.leaseAuthority !== 'local'
&& value.leaseAuthority !== 'hub'
) {
return undefined;
}
if (
value.source !== 'local-file'
&& value.source !== 'hub-projection'
) {
return undefined;
}
return {
version: 1,
authorityRoot: value.authorityRoot,
leaseId: value.leaseId,
ownerNodeId: value.ownerNodeId,
...(readString(value.ownerHostId) ? { ownerHostId: readString(value.ownerHostId) } : {}),
...(readString(value.ownerInstallId) ? { ownerInstallId: readString(value.ownerInstallId) } : {}),
...(readString(value.ownerHostLinkId) ? { ownerHostLinkId: readString(value.ownerHostLinkId) } : {}),
placementEpoch: value.placementEpoch,
acquiredAt: value.acquiredAt,
heartbeatAt: value.heartbeatAt,
expiresAt: value.expiresAt,
status: value.status,
scope: value.scope,
leaseAuthority: value.leaseAuthority,
source: value.source,
...(readString(value.reason) ? { reason: readString(value.reason) } : {}),
};
}
function claimModeForCardinality(cardinality: MatrixMountCardinality): RuntimeInventoryPlacementMetadata['claimMode'] {
if (cardinality === 'authority-singleton') {
return 'authoritative';
}
if (cardinality === 'queue-service') {
return 'pool-member';
}
if (cardinality === 'replicated-read') {
return 'mirror';
}
return 'local-only';
}
function readMountCardinality(value: unknown): MatrixMountCardinality | undefined {
return value === 'authority-singleton'
|| value === 'device-singleton'
|| value === 'device-local-control'
|| value === 'queue-service'
|| value === 'named-instance'
|| value === 'replicated-read'
? value
: undefined;
}
function readJsonObjectIfPresent(filePath: string): Record<string, unknown> | undefined {
if (!fs.existsSync(filePath)) {
return undefined;
}
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')) as unknown;
if (!isRecord(parsed)) {
throw new Error(`Expected JSON object at ${filePath}`);
}
return parsed;
}
function readString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}

View File

@ -0,0 +1,286 @@
import { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import {
LoadedPackageActor,
RuntimeControlActor,
RUNTIME_CONTROL_SERVICE,
loadedPackageMount,
loadedPackageServiceName,
packageKeyFromPackageRef,
runtimeControlMount,
type ILoadedPackageService,
type IRuntimeControlService,
type LoadedPackageManifest,
type LoadedPackageRecord,
type RuntimeControlState,
} from '@open-matrix/system-runtimes';
type MatrixIntrospect = ReturnType<MatrixActor['on$introspect']>;
type MatrixIntrospectChild = NonNullable<MatrixIntrospect['children']>[number];
export type RunnerRuntimeLifecycleState =
| 'starting'
| 'ready'
| 'live'
| 'stopping'
| 'stopped'
| 'failed';
export interface IRunnerRuntimeControlService {
readonly runtimeId: string;
readonly startedAt: string;
getState(): RunnerRuntimeLifecycleState;
getRuntime(): MatrixRuntime;
requestShutdown(reason: string): void;
}
const RUNNER_RUNTIME_CONTROL_SERVICE = 'runner-runtime-control';
export interface IMountedLoadedPackage {
readonly packageRef: string;
readonly packageKey: string;
readonly mount: string;
}
export interface IMountRunnerRuntimeControlOptions {
readonly runtime: MatrixRuntime;
readonly runtimeId: string;
readonly runtimeMount: string;
readonly controlMount: string;
readonly startedAt: string;
getState(): RunnerRuntimeLifecycleState;
requestShutdown(reason: string): void;
/**
* Optional loaded-package descriptor. When provided, a per-loaded-package
* actor is mounted at `system.runtimes.<runtimeId>.packages.<packageKey>`
* alongside the runtime control actor. Slice 1a wires this for runtimes
* that pass a single manifest at startup; multi-package runtimes are
* future work.
*/
readonly loadedPackage?: {
readonly packageRef: string;
readonly version?: string;
readonly manifest?: LoadedPackageManifest;
};
}
export interface IMountRunnerRuntimeControlResult {
readonly controlMount: string;
readonly runtimeMount: string;
readonly loadedPackages: readonly IMountedLoadedPackage[];
}
export class RunnerRuntimeControlActor extends MatrixActor {
static readonly hasDynamicChildren = true;
static description = 'Runtime control actor for Host-managed runner processes.';
static override accepts: Record<string, unknown> = {
'runtime.status': { description: 'Report runtime lifecycle and inventory status', options: 'object?' },
'runtime.ready': { description: 'Report whether the runtime is ready for traffic', options: 'object?' },
'runtime.health': { description: 'Report runtime health', options: 'object?' },
'runtime.shutdown': { description: 'Request graceful runtime shutdown', reason: 'string?' },
};
protected override onIntrospect(payload?: { depth?: 'basic' | 'full' }): MatrixIntrospect {
const base = super.onIntrospect(payload);
const service = this._service();
const currentMount = this.mount;
const children = service.getRuntime().getAllComponents()
.map((component): MatrixIntrospectChild | null => {
const mount = component.mount.trim();
if (!mount || mount === currentMount) {
return null;
}
return {
id: mount,
slot: 'runtime',
mount,
componentClass: component.constructor.name || 'MatrixActor',
accepts: [],
emits: [],
tracks: [],
};
})
.filter((child): child is MatrixIntrospectChild => child !== null)
.sort((left, right) => left.mount.localeCompare(right.mount));
return {
...base,
children: [...(base.children ?? []), ...children],
};
}
onRuntimeStatus(): Record<string, unknown> {
const service = this._service();
const runtime = service.getRuntime();
return {
ok: true,
runtimeId: service.runtimeId,
pid: process.pid,
...(process.env.MATRIX_HOST_HOME ? { hostHome: process.env.MATRIX_HOST_HOME } : {}),
state: service.getState(),
startedAt: service.startedAt,
uptimeMs: Date.now() - Date.parse(service.startedAt),
actorCount: runtime.getAllComponents().length,
mounts: runtime.getAllComponents().map((component) => component.mount),
};
}
onRuntimeReady(): Record<string, unknown> {
const service = this._service();
const state = service.getState();
return {
ok: true,
runtimeId: service.runtimeId,
ready: state === 'ready' || state === 'live',
state,
pid: process.pid,
...(process.env.MATRIX_HOST_HOME ? { hostHome: process.env.MATRIX_HOST_HOME } : {}),
};
}
onRuntimeHealth(): Record<string, unknown> {
const service = this._service();
const state = service.getState();
return {
ok: true,
healthy: state === 'ready' || state === 'live',
runtimeId: service.runtimeId,
state,
pid: process.pid,
...(process.env.MATRIX_HOST_HOME ? { hostHome: process.env.MATRIX_HOST_HOME } : {}),
};
}
onRuntimeShutdown(payload?: { reason?: string }): Record<string, unknown> {
const service = this._service();
const reason = typeof payload?.reason === 'string' && payload.reason.trim().length > 0
? payload.reason.trim()
: 'runtime.shutdown';
setTimeout(() => service.requestShutdown(reason), 0);
return {
ok: true,
accepted: true,
runtimeId: service.runtimeId,
state: 'stopping',
reason,
};
}
private _service(): IRunnerRuntimeControlService {
const service = this.context?.getService<IRunnerRuntimeControlService>(RUNNER_RUNTIME_CONTROL_SERVICE);
if (!service) {
throw new Error('Runner runtime control service is not registered');
}
return service;
}
}
export async function mountRunnerRuntimeControl(
options: IMountRunnerRuntimeControlOptions,
): Promise<IMountRunnerRuntimeControlResult> {
const service: IRunnerRuntimeControlService = {
runtimeId: options.runtimeId,
startedAt: options.startedAt,
getState: options.getState,
getRuntime: () => options.runtime,
requestShutdown: options.requestShutdown,
};
options.runtime.registerService(RUNNER_RUNTIME_CONTROL_SERVICE, service);
const canonicalRuntimeMount = runtimeControlMount(options.runtimeId);
// Build the loaded-package record set (Slice 1a: zero or one package).
const loadedPackages: LoadedPackageRecord[] = [];
if (options.loadedPackage && options.loadedPackage.packageRef.trim().length > 0) {
const packageRef = options.loadedPackage.packageRef.trim();
const packageKey = packageKeyFromPackageRef(packageRef);
const record: LoadedPackageRecord = {
packageRef,
packageKey,
mount: loadedPackageMount(options.runtimeId, packageRef),
state: 'loaded',
loadedAt: Date.parse(options.startedAt) || Date.now(),
...(options.loadedPackage.version ? { version: options.loadedPackage.version } : {}),
...(options.loadedPackage.manifest ? { manifest: options.loadedPackage.manifest } : {}),
};
loadedPackages.push(record);
}
// Register the runtime-control service consumed by RuntimeControlActor.
const runtimeControlService: IRuntimeControlService = {
getRuntimeState: (): RuntimeControlState => ({
runtimeId: options.runtimeId,
...(process.env.MATRIX_RUNTIME_DISPLAY_NAME ? { displayName: process.env.MATRIX_RUNTIME_DISPLAY_NAME } : {}),
...(loadedPackages[0]?.packageRef ? { packageRef: loadedPackages[0].packageRef } : {}),
controlMount: canonicalRuntimeMount,
state: lifecycleStateToControlState(options.getState()),
health: {
status: 'ok',
updatedAt: Date.now(),
},
registeredAt: Date.parse(options.startedAt) || Date.now(),
lastHeartbeat: Date.now(),
}),
getLoadedPackages: () => loadedPackages,
requestShutdown: options.requestShutdown,
requestReload: async () => ({ accepted: false, reason: 'runtime.reload is not implemented in Slice 1a' }),
};
options.runtime.registerService(RUNTIME_CONTROL_SERVICE, runtimeControlService);
// Mount the canonical Runtime control actor at system.runtimes.<runtimeId>.
// Slice 1b (control-actor-nav): the legacy `.control` sibling mount has been
// removed. The canonical runtime control mount is `system.runtimes.<runtimeId>`.
if (!options.runtime.hasComponent(canonicalRuntimeMount)) {
await options.runtime.createSupervised(RuntimeControlActor, canonicalRuntimeMount);
}
// Mount per-loaded-package actors and register their per-package services.
const mountedLoadedPackages: IMountedLoadedPackage[] = [];
for (const record of loadedPackages) {
const loadedPackageService: ILoadedPackageService = {
getRuntimeId: () => options.runtimeId,
getLoadedPackage: () => record,
requestReload: async () => ({ accepted: false, reason: 'package.reload is not implemented in Slice 1a' }),
};
options.runtime.registerService(loadedPackageServiceName(record.packageKey), loadedPackageService);
if (!options.runtime.hasComponent(record.mount)) {
await options.runtime.createSupervised(LoadedPackageActor, record.mount);
}
mountedLoadedPackages.push({
packageRef: record.packageRef,
packageKey: record.packageKey,
mount: record.mount,
});
}
return {
controlMount: canonicalRuntimeMount,
runtimeMount: canonicalRuntimeMount,
loadedPackages: mountedLoadedPackages,
};
}
function lifecycleStateToControlState(state: RunnerRuntimeLifecycleState): RuntimeControlState['state'] {
switch (state) {
case 'starting':
return 'starting';
case 'ready':
case 'live':
return 'running';
case 'stopping':
return 'stopping';
case 'stopped':
case 'failed':
return 'stopped';
}
}
export function defaultRunnerRuntimeMount(runtimeId: string): string {
return `system.runtimes.${runtimeId}`;
}
export function defaultRunnerControlMount(runtimeId: string): string {
// Slice 1b (control-actor-nav): the canonical runtime control mount is
// `system.runtimes.<runtimeId>`. The legacy `.control` sibling is gone.
return defaultRunnerRuntimeMount(runtimeId);
}

View File

@ -0,0 +1,296 @@
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as path from 'node:path';
import { connect as natsConnect, type NatsConnection } from 'nats';
import {
DEFAULT_SECURITY_POLICY,
DEFAULT_SUPERVISION_POLICY,
SecurityRealm,
} from '@open-matrix/core';
import { createPolicyEngine } from '@open-matrix/core/core/security/PolicyPresets';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import { InMemoryBroker } from '@open-matrix/core/transport/InMemoryBroker.js';
import { InMemoryTransport } from '@open-matrix/core/transport/InMemoryTransport.js';
import { NatsTransport } from '@open-matrix/core/transport/NatsTransport.js';
import { authenticatorFromCredentialsRef } from '@open-matrix/nats-auth';
import { SqliteRecordStoreProvider } from '@open-matrix/system-sqlite';
import type { IResolvedRunnerTransportPlan } from './runner-transport.js';
export interface IRunnerRuntimeHandle {
readonly runtime: MatrixRuntime;
readonly transportMode: 'in-memory' | 'external' | 'embedded';
readonly root?: string;
readonly runnerTransport?: IResolvedRunnerTransportPlan;
readonly closed?: Promise<IRunnerRuntimeClosed>;
shutdown(): Promise<void>;
}
export interface IRunnerRuntimeClosed {
readonly error?: string;
}
export function formatRunnerRuntimeClosed(closed: IRunnerRuntimeClosed): string {
const detail = closed.error?.trim();
return detail ? `Runner transport closed: ${detail}` : 'Runner transport closed';
}
export function createRunnerSecurityRealm(): SecurityRealm {
return new SecurityRealm(
DEFAULT_SUPERVISION_POLICY,
DEFAULT_SECURITY_POLICY,
undefined,
undefined,
createPolicyEngine({ policyEngine: 'declarative', preset: 'standard' }),
);
}
export async function createRunnerRuntimeHandle(
root: string | undefined,
runnerTransport?: IResolvedRunnerTransportPlan,
): Promise<IRunnerRuntimeHandle> {
if (!runnerTransport) {
const transport = new InMemoryTransport(new InMemoryBroker(), {
name: 'mx-service-runner',
...(root ? { root } : {}),
});
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
recordStoreProvider: new SqliteRecordStoreProvider(),
});
return {
runtime,
transportMode: 'in-memory',
...(root ? { root } : {}),
async shutdown(): Promise<void> {
await runtime.shutdown();
await transport.disconnect();
},
};
}
if (runnerTransport.mode === 'external') {
const authenticator = await authenticatorFromCredentialsRef(runnerTransport.credentialsRef);
const connection = await natsConnect({
servers: [runnerTransport.url],
name: `mx-runner-${process.pid}`,
timeout: 5_000,
...(authenticator ? { authenticator } : {}),
});
const closed = watchNatsConnectionClosed(connection);
const transport = new NatsTransport(connection, { root: runnerTransport.root });
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
recordStoreProvider: new SqliteRecordStoreProvider(),
});
return {
runtime,
transportMode: 'external',
root: runnerTransport.root,
runnerTransport,
closed,
async shutdown(): Promise<void> {
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
await closed;
},
};
}
const child = await startEmbeddedNatsServer(runnerTransport);
const connection = await natsConnect({
servers: [runnerTransport.url],
name: `mx-runner-embedded-${process.pid}`,
timeout: 5_000,
});
const closed = watchNatsConnectionClosed(connection);
const transport = new NatsTransport(connection, { root: runnerTransport.root });
writePidFile(runnerTransport.pidFile, child.pid);
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
});
return {
runtime,
transportMode: 'embedded',
root: runnerTransport.root,
runnerTransport,
closed,
async shutdown(): Promise<void> {
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
await closed;
await stopChildProcess(child, runnerTransport.pidFile);
},
};
}
function watchNatsConnectionClosed(connection: NatsConnection): Promise<IRunnerRuntimeClosed> {
return connection.closed().then(
(reason): IRunnerRuntimeClosed => {
if (reason instanceof Error) {
return { error: reason.message };
}
return {};
},
(err: unknown): IRunnerRuntimeClosed => ({
error: err instanceof Error ? err.message : String(err),
}),
);
}
async function startEmbeddedNatsServer(
runnerTransport: IResolvedRunnerTransportPlan,
): Promise<ChildProcessWithoutNullStreams> {
if (!runnerTransport.binaryPath) {
throw new Error('Embedded runner transport requires binaryPath');
}
if (!runnerTransport.port) {
throw new Error('Embedded runner transport requires port');
}
if (!runnerTransport.wsPort) {
throw new Error('Embedded runner transport requires wsPort');
}
if (!runnerTransport.dataDir) {
throw new Error('Embedded runner transport requires dataDir');
}
fs.mkdirSync(runnerTransport.dataDir, { recursive: true });
const configPath = path.join(runnerTransport.dataDir, 'nats-server.conf');
const escapedStoreDir = runnerTransport.dataDir.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
fs.writeFileSync(configPath, [
'host: "127.0.0.1"',
`port: ${runnerTransport.port}`,
'jetstream {',
` store_dir: "${escapedStoreDir}"`,
'}',
'websocket {',
' host: "127.0.0.1"',
` port: ${runnerTransport.wsPort}`,
' no_tls: true',
'}',
'',
].join('\n'));
const child = spawn(runnerTransport.binaryPath, ['-c', configPath], {
stdio: 'pipe',
env: process.env,
windowsHide: true,
});
let stderr = '';
let spawnError: Error | null = null;
child.once('error', (error: Error) => {
spawnError = error;
});
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString('utf8');
});
await waitForPorts(
[runnerTransport.port, runnerTransport.wsPort],
10_000,
child,
() => {
if (spawnError) {
return `${spawnError.message}${stderr ? ` | ${stderr.trim()}` : ''}`;
}
return stderr.trim();
},
() => spawnError,
);
return child;
}
async function waitForPorts(
ports: number[],
timeoutMs: number,
child: ChildProcessWithoutNullStreams,
getLogs: () => string,
getSpawnError?: () => Error | null,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const spawnError = getSpawnError?.();
if (spawnError) {
throw new Error(`Failed to spawn nats-server: ${spawnError.message}`);
}
if (child.exitCode != null) {
throw new Error(`nats-server exited early (${child.exitCode}): ${getLogs()}`);
}
const allListening = await Promise.all(ports.map((port) => isPortListening(port)));
if (allListening.every(Boolean)) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`Timed out waiting for nats-server on ports ${ports.join(', ')}: ${getLogs()}`);
}
async function isPortListening(port: number): Promise<boolean> {
return await new Promise((resolve) => {
const socket = net.connect({ host: '127.0.0.1', port });
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
socket.once('error', () => {
socket.destroy();
resolve(false);
});
socket.setTimeout(500, () => {
socket.destroy();
resolve(false);
});
});
}
async function stopChildProcess(
child: ChildProcessWithoutNullStreams,
pidFile?: string,
): Promise<void> {
if (child.exitCode == null) {
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (child.exitCode == null) {
child.kill('SIGKILL');
}
}, 2_000);
child.once('exit', () => {
clearTimeout(timeout);
resolve();
});
child.kill('SIGTERM');
});
}
if (pidFile) {
fs.rmSync(pidFile, { force: true });
}
}
function writePidFile(pidFile: string | undefined, pid: number | undefined): void {
if (!pidFile || !pid) {
return;
}
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
fs.writeFileSync(pidFile, `${pid}\n`, 'utf8');
}

View File

@ -0,0 +1,685 @@
import * as fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { buildMountedInstanceBootstrapProps } from '@open-matrix/core/runtime/InstanceBootstrapContext.js';
import {
buildMatrixHttpAssetMount,
MatrixHttpAssetEndpointActor,
} from '@open-matrix/system-gateway-http';
import { HostAuthStateStore, HostCredentialStore } from '@open-matrix/system-auth';
import {
loadBootstrapJsonObject,
resolveBootstrapRefPath,
loadBootstrapJsonValue,
} from '@open-matrix/core/runtime/InstanceBootstrapRefs.js';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import { loadMatrixServiceManifest } from '../utils/service-manifest.js';
import {
loadPackageEnvironment,
loadPackageEnvironmentFromPath,
type IMatrixPackageEnvironment,
} from '../utils/package-environment.js';
import {
deregisterRunnerBindings,
deregisterRunnerRuntime,
heartbeatRunnerRuntime,
type IRunnerWebappRouteMetadata,
registerRunnerBindings,
type IRunnerBindingDeregistration,
type IRunnerBindingRegistration,
registerRunnerRuntime,
renewRunnerBindings,
resolveRunnerRuntimeId,
type IRunnerRuntimeDeregistration,
type IRunnerRuntimeRegistration,
} from '../utils/runner-control-plane.js';
import {
defaultRunnerRuntimeMount,
mountRunnerRuntimeControl,
type RunnerRuntimeLifecycleState,
} from '../utils/runner-runtime-control.js';
import { resolveRunnerTransportPlan, type IResolvedRunnerTransportPlan } from '../utils/runner-transport.js';
import {
createRunnerRuntimeHandle,
formatRunnerRuntimeClosed,
type IRunnerRuntimeClosed,
type IRunnerRuntimeHandle,
} from '../utils/runner-runtime.js';
import {
loadMatrixWebappManifest,
type ILoadedMatrixWebappManifest,
} from '../utils/webapp-manifest.js';
import { installRunnerPlacementPolicy } from '../utils/runner-placement-policy.js';
export interface IServiceCommandOptions {
readonly check?: boolean;
readonly json?: boolean;
readonly env?: string;
readonly envFile?: string;
readonly overrides?: string;
readonly entry?: string;
readonly export?: string;
readonly root?: string;
readonly mount?: string;
readonly config?: string;
readonly secrets?: string;
}
export interface IServiceCommandResult {
readonly packageName: string;
readonly packageDir: string;
readonly manifestPath: string;
readonly entryPath: string;
readonly exportName: string;
readonly checked: boolean;
readonly environmentName?: string;
readonly environmentPath?: string;
readonly runnerTransport?: IResolvedRunnerTransportPlan;
readonly overrides: Record<string, unknown>;
readonly root?: string;
readonly mount?: string;
readonly configRef?: string;
readonly secretsRef?: string;
readonly instance: Record<string, unknown>;
readonly serviceInstance: Record<string, unknown>;
readonly deploymentInstance: Record<string, unknown>;
readonly runtimeRegistration?: IRunnerRuntimeRegistration;
readonly bindingRegistration?: IRunnerBindingRegistration;
readonly runtimeDeregistration?: IRunnerRuntimeDeregistration;
readonly bindingDeregistration?: IRunnerBindingDeregistration;
readonly result?: unknown;
}
export interface IStartedServiceCommand {
readonly result: IServiceCommandResult;
readonly shutdownRequested: Promise<string>;
readonly transportClosed?: Promise<IRunnerRuntimeClosed>;
shutdown(): Promise<IServiceCommandResult>;
}
type BootstrapResult = {
runtime?: {
shutdown?: () => Promise<void> | void;
};
} & Record<string, unknown>;
interface IStandaloneRunnerServices {
readonly runtime: MatrixRuntime;
readonly environment?: IMatrixPackageEnvironment;
readonly runnerTransport?: IResolvedRunnerTransportPlan;
}
export async function serviceCommand(
packageDir: string,
options: IServiceCommandOptions = {},
): Promise<IServiceCommandResult> {
const started = await startServiceCommand(packageDir, options);
if (options.check) {
return await started.shutdown();
}
if (options.json) {
console.log(JSON.stringify(started.result, null, 2));
} else {
console.log(`[mx service] started ${started.result.packageName} via ${started.result.exportName}`);
console.log(`[mx service] manifest: ${started.result.manifestPath}`);
console.log(`[mx service] entry: ${started.result.entryPath}`);
}
let finalResult = started.result;
const shutdownRequested = started.transportClosed
? Promise.race([
started.shutdownRequested,
started.transportClosed.then((closed) => {
throw new Error(formatRunnerRuntimeClosed(closed));
}),
])
: started.shutdownRequested;
await waitForShutdownSignal(async () => {
finalResult = await started.shutdown();
}, shutdownRequested);
return finalResult;
}
export async function startServiceCommand(
packageDir: string,
options: IServiceCommandOptions = {},
): Promise<IStartedServiceCommand> {
const loaded = loadMatrixServiceManifest(packageDir);
const webapp = loadMatrixWebappManifest(loaded.packageDir);
const envFile = options.envFile?.trim();
const selectedEnvironment = envFile
? loadPackageEnvironmentFromPath(packageDir, envFile, options.env)
: options.env?.trim()
? loadPackageEnvironment(packageDir, options.env)
: undefined;
const entryPath = options.entry
? loadBootstrapEntryPath(loaded.packageDir, options.entry, 'service entry override')
: loaded.entryPath;
const exportName = options.export?.trim() || loaded.exportName;
const root = options.root?.trim()
|| selectedEnvironment?.environment.runtime.root
|| loaded.root;
const runnerTransport = selectedEnvironment
? resolveRunnerTransportPlan(loaded.packageDir, selectedEnvironment, root)
: undefined;
const mount = options.mount?.trim() || loaded.mount;
const configRef = options.config?.trim() || loaded.configRef;
const config = configRef ? loadBootstrapJsonObject(loaded.packageDir, configRef, 'service config') : {};
const manifestOverrides = deepMerge(config, loaded.overrides);
const cliOverrides = parseOverrides(options.overrides);
const effectiveOverrides = deepMerge(manifestOverrides, cliOverrides);
const secretsRef = options.secrets?.trim() || undefined;
const secrets = secretsRef ? loadBootstrapJsonValue(loaded.packageDir, secretsRef, 'service secrets') : undefined;
const mountedInstanceBootstrap = buildMountedInstanceBootstrapProps({
...loaded.serviceInstance,
...(mount ? { mount } : {}),
...(configRef ? { configRef } : {}),
...(Array.isArray(secrets)
? { secretRefs: structuredClone(secrets) }
: loaded.serviceInstance.secretRefs
? { secretRefs: structuredClone(loaded.serviceInstance.secretRefs) }
: {}),
}, effectiveOverrides);
const { instance, serviceInstance, deploymentInstance } = mountedInstanceBootstrap;
const bootstrapContext = {
packageName: loaded.packageName,
packageDir: loaded.packageDir,
manifestPath: loaded.manifestPath,
entryPath,
exportName,
...(root ? { root } : {}),
...(mount ? { mount } : {}),
...(configRef ? { configRef } : {}),
...(secretsRef ? { secretsRef } : {}),
...(secrets !== undefined ? { secrets } : {}),
...(loaded.transport ? { transport: loaded.transport } : {}),
...(loaded.auth ? { auth: loaded.auth } : {}),
...(loaded.http ? { http: loaded.http } : {}),
...(selectedEnvironment ? {
environmentName: selectedEnvironment.envName,
environmentPath: selectedEnvironment.environmentPath,
environment: selectedEnvironment.environment,
} : {}),
...(runnerTransport ? { runnerTransport } : {}),
...mountedInstanceBootstrap,
};
const runnerRuntimeHandle = await createRunnerRuntimeHandle(root, runnerTransport);
const runnerRuntime = runnerRuntimeHandle.runtime;
registerHostHomeService(runnerRuntime, selectedEnvironment?.environment.host?.matrixDir);
const runtimeId = resolveRunnerRuntimeId(loaded, selectedEnvironment);
const runtimeMount = selectedEnvironment?.environment.runtime.runtimeMount
?? process.env.MATRIX_RUNTIME_MOUNT
?? defaultRunnerRuntimeMount(runtimeId);
// Slice 1b (control-actor-nav): control authority is the canonical
// per-runtime mount, not the legacy `.control` sibling.
const controlMount = selectedEnvironment?.environment.runtime.controlMount
?? process.env.MATRIX_RUNTIME_CONTROL_MOUNT
?? runtimeMount;
let lifecycleState: RunnerRuntimeLifecycleState = 'starting';
let requestRuntimeShutdown: (reason: string) => void = () => undefined;
const shutdownRequested = new Promise<string>((resolve) => {
requestRuntimeShutdown = resolve;
});
const runnerServices: IStandaloneRunnerServices = {
runtime: runnerRuntime,
...(selectedEnvironment ? { environment: selectedEnvironment.environment } : {}),
...(runnerTransport ? { runnerTransport } : {}),
};
installRunnerPlacementPolicy({
runtime: runnerRuntime,
manifest: loaded,
environment: selectedEnvironment,
runtimeId,
authorityRoot: root,
});
const moduleUrl = `${pathToFileURL(entryPath).href}?mx_service=${Date.now()}-${Math.random().toString(16).slice(2)}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const bootstrap = mod[exportName];
if (typeof bootstrap !== 'function') {
throw new Error(`Package runtime factory export "${exportName}" was not found in ${entryPath}`);
}
const bootstrapResult = await (bootstrap as (...args: unknown[]) => Promise<unknown> | unknown)(
effectiveOverrides,
runnerServices,
undefined,
bootstrapContext,
);
const activeRuntime = resolveBootstrapRuntime(bootstrapResult) ?? runnerRuntime;
const webappAssetMount = webapp
? await mountMatrixHttpAssetEndpoint(activeRuntime, webapp, runtimeId)
: undefined;
const mounted = await mountRunnerRuntimeControl({
runtime: activeRuntime,
runtimeId,
runtimeMount,
controlMount,
startedAt: new Date().toISOString(),
getState: () => lifecycleState,
requestShutdown: (reason) => requestRuntimeShutdown(reason),
loadedPackage: {
packageRef: loaded.packageName,
manifest: {
packageRef: loaded.packageName,
...(loaded.mount ? { mount: loaded.mount } : {}),
packageDir: loaded.packageDir,
},
},
});
const mountedControlMount = mounted.controlMount;
lifecycleState = 'ready';
const runtimeRegistration = await registerRunnerRuntime(
activeRuntime,
loaded,
selectedEnvironment,
runnerTransport,
mountedInstanceBootstrap.serviceInstance.mount,
runtimeMount,
mountedControlMount,
webapp && webappAssetMount ? buildRunnerWebappRouteMetadata(webapp, webappAssetMount) : undefined,
);
const bindingRegistration = await registerRunnerBindings(
activeRuntime,
loaded,
selectedEnvironment,
runtimeRegistration.runtimeId,
mountedInstanceBootstrap.serviceInstance.mount,
);
lifecycleState = 'live';
const serializedInstance = sanitizeForJson(instance) as Record<string, unknown>;
let runtimeDeregistration: IRunnerRuntimeDeregistration | undefined;
let bindingDeregistration: IRunnerBindingDeregistration | undefined;
// Heartbeat tick + self-healing reclaim.
//
// registry.claim.renew (inside heartbeatRunnerRuntime / renewRunnerBindings)
// returns the count of claims the server-side registry actually refreshed.
// 0 means "the registry has no record of your claim" — typically because
// the registry actor's in-memory _claims map was wiped when the SYSTEM
// runtime restarted. Without reclaim the original claim's TTL (45s)
// expires, the runtime vanishes from registry.query, the gateway can't
// route to it, and apps appear "not registered" until manual intervention.
//
// We re-issue exactly what we issued at registration time. Server-side ops
// are idempotent (onRegistryClaim preserves registeredAt/claimedAt for
// existing entries; bindings.register is keyed by provider+match), so
// reclaiming against a healthy registry is harmless.
//
// Log discipline: log STATE TRANSITIONS, not every tick. First failure
// logs; subsequent consecutive failures at the same attempt count stay
// silent; every 10th attempt logs that we're still trying; recovery from
// a degraded state logs the duration. Healthy ticks log nothing.
let runtimeReclaimAttempts = 0;
let bindingReclaimAttempts = 0;
const logIfTransitionOrMilestone = (
attempts: number,
onFirst: () => void,
onMilestone: () => void,
): void => {
if (attempts === 1) {
onFirst();
} else if (attempts % 10 === 0) {
onMilestone();
}
};
const runtimePresenceTimer = options.check === true
? undefined
: setInterval(() => {
void (async () => {
try {
const [heartbeat, renewal] = await Promise.all([
heartbeatRunnerRuntime(activeRuntime, runtimeRegistration),
renewRunnerBindings(activeRuntime, bindingRegistration),
]);
if (heartbeat && !heartbeat.known) {
runtimeReclaimAttempts += 1;
logIfTransitionOrMilestone(
runtimeReclaimAttempts,
() => console.error(
`[mx service] registry forgot runtime claim for ${heartbeat.runtimeId} (renewed=0); reclaiming`,
),
() => console.error(
`[mx service] still reclaiming runtime ${heartbeat.runtimeId} after ${runtimeReclaimAttempts} attempts`,
),
);
try {
const reclaimed = await runtimeRegistration.reclaim();
if (runtimeReclaimAttempts > 0) {
console.error(
`[mx service] runtime reclaim recovered for ${heartbeat.runtimeId} after ${runtimeReclaimAttempts} attempt(s): ${reclaimed.inventoryClaimCount} inventory claims re-issued`,
);
}
runtimeReclaimAttempts = 0;
} catch (reclaimErr) {
logIfTransitionOrMilestone(
runtimeReclaimAttempts,
() => console.error(
`[mx service] runtime reclaim failed for ${heartbeat.runtimeId}: ${reclaimErr instanceof Error ? reclaimErr.message : String(reclaimErr)} (will retry next tick)`,
),
() => console.error(
`[mx service] runtime reclaim still failing for ${heartbeat.runtimeId} after ${runtimeReclaimAttempts} attempts: ${reclaimErr instanceof Error ? reclaimErr.message : String(reclaimErr)}`,
),
);
}
} else if (heartbeat && runtimeReclaimAttempts > 0) {
// Registry started answering renew=N>0 without us reclaiming
// first — extremely rare (the registry would need to repopulate
// claims from somewhere) but log the recovery so operators see
// it. Reset the counter.
console.error(
`[mx service] runtime registry recovered without explicit reclaim for ${heartbeat.runtimeId} after ${runtimeReclaimAttempts} attempt(s)`,
);
runtimeReclaimAttempts = 0;
}
if (renewal && renewal.refreshed === 0 && (bindingRegistration?.bindings.length ?? 0) > 0) {
bindingReclaimAttempts += 1;
logIfTransitionOrMilestone(
bindingReclaimAttempts,
() => console.error(
`[mx service] registry forgot binding claims for ${renewal.runtimeId} (refreshed=0); reclaiming`,
),
() => console.error(
`[mx service] still reclaiming bindings for ${renewal.runtimeId} after ${bindingReclaimAttempts} attempts`,
),
);
try {
const reclaimed = await bindingRegistration?.reclaim();
if (reclaimed && bindingReclaimAttempts > 0) {
console.error(
`[mx service] binding reclaim recovered for ${renewal.runtimeId} after ${bindingReclaimAttempts} attempt(s): ${reclaimed.registryClaimCount} registry claims + ${reclaimed.bindingsRegisteredCount} binding forwards re-issued`,
);
}
bindingReclaimAttempts = 0;
} catch (reclaimErr) {
logIfTransitionOrMilestone(
bindingReclaimAttempts,
() => console.error(
`[mx service] binding reclaim failed for ${renewal.runtimeId}: ${reclaimErr instanceof Error ? reclaimErr.message : String(reclaimErr)} (will retry next tick)`,
),
() => console.error(
`[mx service] binding reclaim still failing for ${renewal.runtimeId} after ${bindingReclaimAttempts} attempts: ${reclaimErr instanceof Error ? reclaimErr.message : String(reclaimErr)}`,
),
);
}
} else if (renewal && bindingReclaimAttempts > 0) {
console.error(
`[mx service] binding registry recovered without explicit reclaim for ${renewal.runtimeId} after ${bindingReclaimAttempts} attempt(s)`,
);
bindingReclaimAttempts = 0;
}
} catch (err) {
console.error(
`[mx service] runtime presence tick failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
})();
}, 10_000);
const buildResult = (): IServiceCommandResult => ({
packageName: loaded.packageName,
packageDir: loaded.packageDir,
manifestPath: loaded.manifestPath,
entryPath,
exportName,
checked: options.check === true,
...(selectedEnvironment ? {
environmentName: selectedEnvironment.envName,
environmentPath: selectedEnvironment.environmentPath,
} : {}),
...(runnerTransport ? { runnerTransport } : {}),
overrides: effectiveOverrides,
...(root ? { root } : {}),
...(mount ? { mount } : {}),
...(configRef ? { configRef } : {}),
...(secretsRef ? { secretsRef } : {}),
instance: serializedInstance,
serviceInstance: structuredClone(serializedInstance),
deploymentInstance: structuredClone(serializedInstance),
runtimeRegistration,
bindingRegistration,
...(runtimeDeregistration ? { runtimeDeregistration } : {}),
...(bindingDeregistration ? { bindingDeregistration } : {}),
result: sanitizeForJson(bootstrapResult),
});
let shutdownPromise: Promise<IServiceCommandResult> | null = null;
return {
result: buildResult(),
...(runnerRuntimeHandle.closed ? { transportClosed: runnerRuntimeHandle.closed } : {}),
async shutdown(): Promise<IServiceCommandResult> {
if (!shutdownPromise) {
shutdownPromise = (async () => {
lifecycleState = 'stopping';
if (runtimePresenceTimer) {
clearInterval(runtimePresenceTimer);
}
({ runtimeDeregistration, bindingDeregistration } = await shutdownBootstrapRuntime(
bootstrapResult,
activeRuntime,
runtimeRegistration,
bindingRegistration,
runnerRuntimeHandle,
));
lifecycleState = 'stopped';
return buildResult();
})();
}
return await shutdownPromise;
},
shutdownRequested,
};
}
function registerHostHomeService(
runtime: MatrixRuntime,
matrixDir: string | undefined,
): void {
const hostHome = matrixDir?.trim();
if (hostHome) {
runtime.registerService('matrix-host-home', hostHome);
runtime.registerService('credential-store', new HostCredentialStore(new HostAuthStateStore(hostHome)));
}
}
async function mountMatrixHttpAssetEndpoint(
runtime: MatrixRuntime,
webapp: ILoadedMatrixWebappManifest,
runtimeId?: string,
): Promise<string> {
const assetMount = buildMatrixHttpAssetMount(webapp.appName, runtimeId);
if (!runtime.hasComponent(assetMount)) {
await runtime.createSupervised(MatrixHttpAssetEndpointActor as unknown as new () => MatrixActor, assetMount, {
assetEndpoint: {
packageName: webapp.packageName,
appName: webapp.appName,
distDir: webapp.distDir,
entryFile: webapp.entryFile,
},
});
}
return assetMount;
}
function buildRunnerWebappRouteMetadata(
webapp: ILoadedMatrixWebappManifest,
assetMount: string,
): IRunnerWebappRouteMetadata {
return {
appName: webapp.appName,
routePrefix: `/apps/${webapp.appName}/`,
...(webapp.displayName ? { displayName: webapp.displayName } : {}),
...(webapp.icon ? { icon: webapp.icon } : {}),
...(typeof webapp.navOrder === 'number' ? { navOrder: webapp.navOrder } : {}),
...(webapp.description ? { description: webapp.description } : {}),
...(webapp.shells && webapp.shells.length > 0 ? { shells: webapp.shells } : {}),
assetMount,
};
}
function parseOverrides(raw: string | undefined): Record<string, unknown> {
if (!raw) return {};
try {
const parsed = JSON.parse(raw) as unknown;
if (!isRecord(parsed)) {
throw new Error('overrides must be a JSON object');
}
return parsed;
} catch (err) {
throw new Error(`Failed to parse service overrides: ${err instanceof Error ? err.message : String(err)}`);
}
}
function deepMerge(
base: Record<string, unknown>,
patch: Record<string, unknown>,
): Record<string, unknown> {
const output = structuredClone(base);
for (const [key, value] of Object.entries(patch)) {
const current = output[key];
if (isRecord(current) && isRecord(value)) {
output[key] = deepMerge(current, value);
continue;
}
output[key] = value;
}
return output;
}
function loadBootstrapEntryPath(packageDir: string, ref: string, label: string): string {
const entryPath = ref.trim();
if (entryPath.length === 0) {
throw new Error(`${label} must be a non-empty path`);
}
const filePath = resolveBootstrapRefPath(packageDir, entryPath);
if (!fs.existsSync(filePath)) {
throw new Error(`${label} not found at ${filePath}`);
}
return filePath;
}
async function shutdownBootstrapRuntime(
result: unknown,
activeRuntime?: MatrixRuntime,
registration?: IRunnerRuntimeRegistration,
bindingRegistration?: IRunnerBindingRegistration,
runnerRuntimeHandle?: IRunnerRuntimeHandle,
): Promise<{
readonly runtimeDeregistration?: IRunnerRuntimeDeregistration;
readonly bindingDeregistration?: IRunnerBindingDeregistration;
}> {
let shutdownHandled = false;
let runtimeDeregistration: IRunnerRuntimeDeregistration | undefined;
let bindingDeregistration: IRunnerBindingDeregistration | undefined;
if (isRecord(result)) {
const runtime: unknown = result.runtime;
if (isRecord(runtime)) {
const shutdown = runtime.shutdown;
if (typeof shutdown === 'function') {
if (activeRuntime) {
bindingDeregistration = await deregisterRunnerBindings(activeRuntime, bindingRegistration);
runtimeDeregistration = await deregisterRunnerRuntime(activeRuntime, registration);
}
await shutdown.call(runtime);
shutdownHandled = Object.is(runtime, runnerRuntimeHandle?.runtime);
}
}
}
if (runnerRuntimeHandle && !shutdownHandled) {
if (!runtimeDeregistration && activeRuntime) {
bindingDeregistration = await deregisterRunnerBindings(activeRuntime, bindingRegistration);
runtimeDeregistration = await deregisterRunnerRuntime(activeRuntime, registration);
}
await runnerRuntimeHandle.shutdown();
}
return {
...(runtimeDeregistration ? { runtimeDeregistration } : {}),
...(bindingDeregistration ? { bindingDeregistration } : {}),
};
}
function resolveBootstrapRuntime(result: unknown): MatrixRuntime | undefined {
if (!isRecord(result)) {
return undefined;
}
const runtime = result.runtime;
return runtime instanceof MatrixRuntime ? runtime : undefined;
}
function sanitizeForJson(value: unknown, depth = 8, seen = new WeakSet<object>()): unknown {
if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (Array.isArray(value)) {
if (depth <= 0) return `[array:${value.length}]`;
return value
.map((entry) => sanitizeForJson(entry, depth - 1, seen))
.filter((entry) => entry !== undefined);
}
if (!isRecord(value)) {
return String(value);
}
if (seen.has(value)) {
return '[circular]';
}
seen.add(value);
if (depth <= 0) {
return '[object]';
}
const output: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
const sanitized = sanitizeForJson(entry, depth - 1, seen);
if (sanitized !== undefined) {
output[key] = sanitized;
}
}
return output;
}
async function waitForShutdownSignal(
onShutdown: () => Promise<void>,
shutdownRequested?: Promise<string>,
): Promise<void> {
await new Promise<void>((resolve, reject) => {
let settled = false;
const complete = (err?: unknown) => {
if (settled) return;
settled = true;
process.off('SIGINT', handleSignal);
process.off('SIGTERM', handleSignal);
if (err) {
reject(err);
return;
}
resolve();
};
const handleSignal = () => {
void onShutdown()
.then(() => complete())
.catch((err) => complete(err));
};
process.on('SIGINT', handleSignal);
process.on('SIGTERM', handleSignal);
shutdownRequested
?.then(() => handleSignal())
.catch((err) => complete(err));
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View File

@ -0,0 +1,522 @@
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as net from 'node:net';
import * as path from 'node:path';
import * as tls from 'node:tls';
import type { AddressInfo } from 'node:net';
import type { Duplex } from 'node:stream';
import type { ILoadedMatrixPackageEnvironment } from './package-environment.js';
import type { IResolvedRunnerTransportPlan } from './runner-transport.js';
import type { ILoadedMatrixWebappManifest } from './webapp-manifest.js';
import { jwtCredentialsFromCredentialsRef } from '@open-matrix/nats-auth';
export interface IStandaloneWebappServerOptions {
readonly loadedEnvironment: ILoadedMatrixPackageEnvironment;
readonly webapp: ILoadedMatrixWebappManifest;
readonly runnerTransport: IResolvedRunnerTransportPlan;
}
export interface IStandaloneWebappServerHandle {
readonly port: number;
readonly baseUrl: string;
readonly webapp: ILoadedMatrixWebappManifest;
readonly bootstrapPath: '/api/bootstrap';
readonly natsWsPath: '/nats-ws';
shutdown(): Promise<void>;
}
interface IMimeTypeMap {
readonly [key: string]: string;
}
const MIME_TYPES: IMimeTypeMap = {
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.ico': 'image/x-icon',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.txt': 'text/plain; charset=utf-8',
};
export async function startStandaloneWebappServer(
options: IStandaloneWebappServerOptions,
): Promise<IStandaloneWebappServerHandle> {
const httpConfig = options.loadedEnvironment.environment.http;
if (httpConfig?.enabled !== true || typeof httpConfig.port !== 'number' || httpConfig.port <= 0) {
throw new Error(
`Package environment "${options.loadedEnvironment.envName}" must enable http.port for --serve mode`,
);
}
if (!fs.existsSync(options.webapp.distDir)) {
throw new Error(
`Built webapp dist directory not found for ${options.webapp.packageName}: ${options.webapp.distDir}`,
);
}
const entryPath = path.join(options.webapp.distDir, options.webapp.entryFile);
if (!fs.existsSync(entryPath)) {
throw new Error(
`Built webapp entry file not found for ${options.webapp.packageName}: ${entryPath}`,
);
}
if (!options.runnerTransport.wsUrl) {
throw new Error(
`Runner transport for ${options.webapp.packageName} does not expose wsUrl required for /nats-ws proxy`,
);
}
const runtimeRoot = options.runnerTransport.root;
const brandingDir = resolveBrandingDir(options.loadedEnvironment.packageDir);
const server = http.createServer((req, res) => {
void handleRequest(req, res, {
entryPath,
brandingDir,
runtimeRoot,
webapp: options.webapp,
loadedEnvironment: options.loadedEnvironment,
runnerTransport: options.runnerTransport,
}).catch((error) => {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}));
});
});
server.on('upgrade', (req, socket, head) => {
if ((req.url ?? '').split('?')[0] !== '/nats-ws') {
socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
socket.destroy();
return;
}
proxyWebSocketUpgrade(req, socket, head, options.runnerTransport.wsUrl!);
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(httpConfig.port, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
const address = server.address();
if (!address || typeof address !== 'object') {
throw new Error(`Standalone webapp server for ${options.webapp.packageName} did not expose a TCP address`);
}
const port = (address as AddressInfo).port;
return {
port,
baseUrl: `http://127.0.0.1:${port}`,
webapp: options.webapp,
bootstrapPath: '/api/bootstrap',
natsWsPath: '/nats-ws',
async shutdown(): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
async function handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse<http.IncomingMessage>,
options: {
readonly entryPath: string;
readonly brandingDir: string | null;
readonly runtimeRoot: string;
readonly webapp: ILoadedMatrixWebappManifest;
readonly loadedEnvironment: ILoadedMatrixPackageEnvironment;
readonly runnerTransport: IResolvedRunnerTransportPlan;
},
): Promise<void> {
const method = req.method ?? 'GET';
const requestUrl = new URL(req.url ?? '/', 'http://127.0.0.1');
const pathname = requestUrl.pathname;
const isNatsAuthRequest = pathname === '/api/nats-jwt' && method === 'POST';
if (method !== 'GET' && method !== 'HEAD' && !isNatsAuthRequest) {
res.statusCode = 405;
res.setHeader('Allow', pathname === '/api/nats-jwt' ? 'GET, HEAD, POST' : 'GET, HEAD');
res.end();
return;
}
if (pathname === '/api/bootstrap') {
const payload = buildBootstrapPayload(req, options.runtimeRoot, options.runnerTransport, options.loadedEnvironment);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
writeBody(res, method, JSON.stringify(payload));
return;
}
if (pathname === '/api/auth/me') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
writeBody(res, method, JSON.stringify({
authenticated: true,
principal: {
id: 'local-runner',
displayName: 'Local Runner',
email: null,
},
session: {
sub: 'local-runner',
email: null,
exp: null,
localClient: true,
},
}));
return;
}
if (pathname === '/api/config') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
writeBody(res, method, JSON.stringify({
root: options.runtimeRoot,
environment: options.loadedEnvironment.environment.name,
transport: {
mode: options.runnerTransport.mode,
url: options.runnerTransport.url,
wsUrl: options.runnerTransport.wsUrl ?? null,
},
}));
return;
}
if (pathname === '/api/nats-jwt') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
writeBody(res, method, JSON.stringify(buildNatsAuthPayload(
options.runnerTransport,
options.loadedEnvironment.packageDir,
)));
return;
}
if (pathname === '/healthz') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
writeBody(res, method, JSON.stringify({
ok: true,
ready: true,
phase: 'ready',
root: options.runtimeRoot,
}));
return;
}
if (pathname === '/nats-ws') {
res.statusCode = 426;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
writeBody(res, method, 'Upgrade Required');
return;
}
if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname.startsWith('/branding/')) {
const assetPath = resolveBrandingAssetPath(pathname, options.brandingDir);
if (!assetPath) {
res.statusCode = 404;
res.end();
return;
}
await serveFile(res, method, assetPath);
return;
}
const resolvedPath = resolveStaticAssetPath(pathname, options.webapp, options.entryPath);
if (!resolvedPath) {
res.statusCode = 404;
res.end();
return;
}
await serveFile(res, method, resolvedPath);
}
function buildBootstrapPayload(
req: http.IncomingMessage,
runtimeRoot: string,
runnerTransport: IResolvedRunnerTransportPlan,
loadedEnvironment: ILoadedMatrixPackageEnvironment,
): Record<string, unknown> {
const hostHeader = Array.isArray(req.headers.host)
? req.headers.host[0]
: req.headers.host;
const host = hostHeader || `127.0.0.1:${loadedEnvironment.environment.http?.port ?? 0}`;
const protocol = 'ws:';
const wsUrl = `${protocol}//${host}/nats-ws`;
const hasCredentialsRef = typeof runnerTransport.credentialsRef === 'string'
&& runnerTransport.credentialsRef.trim().length > 0;
const authorityContext = {
assetHostRoot: runtimeRoot,
platformServiceRoot: runtimeRoot,
sessionAuthorityRoot: runtimeRoot,
selectedAuthorityRoot: runtimeRoot,
runtimeInstanceRoot: null,
isAuthenticated: true,
isPlatformAdmin: false,
selectableAuthorityRoots: [runtimeRoot],
};
return {
authorityContext,
natsWsUrl: wsUrl,
root: runtimeRoot,
authorityRoot: runtimeRoot,
assetHostRoot: runtimeRoot,
platformServiceRoot: runtimeRoot,
sessionAuthorityRoot: runtimeRoot,
selectedAuthorityRoot: runtimeRoot,
runtimeInstanceRoot: null,
selectableAuthorityRoots: [runtimeRoot],
isPlatformAdmin: false,
busRoot: runtimeRoot,
platformRoot: runtimeRoot,
role: 'client',
addressRoot: runtimeRoot,
provisioned: true,
authorityReady: true,
authenticated: true,
principal: {
principalId: 'local-runner',
displayName: 'Local Runner',
},
natsOwnership: {
mode: runnerTransport.mode,
source: 'package-environment:nats',
},
federation: {
connected: false,
hubUrl: null,
source: 'none',
},
transportPlan: {
protocolVersion: 1,
kind: 'nats',
wsMode: 'same-origin-proxy',
wsPath: '/nats-ws',
authMode: hasCredentialsRef ? 'jwt' : 'anonymous',
authEndpoint: hasCredentialsRef ? '/api/nats-jwt' : null,
busAuthMode: 'none',
sessionMode: 'local-client',
},
sources: {
root: 'package-environment:runtime.root',
authorityRoot: 'package-environment:runtime.root',
assetHostRoot: 'package-environment:runtime.root',
platformServiceRoot: 'package-environment:runtime.root',
sessionAuthorityRoot: 'package-environment:runtime.root',
selectedAuthorityRoot: 'package-environment:runtime.root',
runtimeInstanceRoot: 'none',
platformRoot: 'package-environment:runtime.root',
addressRoot: 'package-environment:runtime.root',
browserWsPath: 'transportPlan.wsPath',
transportAuthMode: hasCredentialsRef ? 'credentials-ref' : 'standalone-runner',
busToken: 'none',
sessionIdentity: 'local-client',
federation: 'none',
},
};
}
function buildNatsAuthPayload(
runnerTransport: IResolvedRunnerTransportPlan,
baseDir: string,
): Record<string, unknown> {
const credentials = jwtCredentialsFromCredentialsRef(
runnerTransport.credentialsRef,
baseDir,
`matrix-browser-${process.pid}`,
);
if (!credentials) {
return { mode: 'anonymous' };
}
return {
mode: 'jwt',
jwt: credentials.jwt,
seed: credentials.seed,
};
}
function resolveStaticAssetPath(
pathname: string,
webapp: ILoadedMatrixWebappManifest,
entryPath: string,
): string | null {
const decodedPath = safeDecodePath(pathname);
if (!decodedPath) {
return null;
}
const appBasePath = `/apps/${webapp.appName}`;
const cleanPath = decodedPath === appBasePath || decodedPath.startsWith(`${appBasePath}/`)
? decodedPath.slice(appBasePath.length) || '/'
: decodedPath;
if (cleanPath === '/') {
return entryPath;
}
const relativePath = cleanPath.replace(/^\/+/, '');
const distRoot = path.resolve(webapp.distDir);
const resolved = path.resolve(webapp.distDir, relativePath);
if (resolved.startsWith(distRoot) && fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
return resolved;
}
if (!path.extname(relativePath)) {
return entryPath;
}
return null;
}
function resolveBrandingAssetPath(pathname: string, brandingDir: string | null): string | null {
if (!brandingDir) {
return null;
}
let assetPath: string;
if (pathname === '/favicon.ico') {
assetPath = path.join(brandingDir, 'favicon.ico');
} else if (pathname === '/manifest.json') {
assetPath = path.join(brandingDir, 'manifest.json');
} else {
const relativePath = safeDecodePath(pathname.slice('/branding/'.length));
if (!relativePath) {
return null;
}
assetPath = path.join(brandingDir, relativePath);
}
const resolved = path.resolve(assetPath);
if (!resolved.startsWith(path.resolve(brandingDir)) || !fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
return null;
}
return resolved;
}
async function serveFile(
res: http.ServerResponse<http.IncomingMessage>,
method: string,
filePath: string,
): Promise<void> {
const stat = await fs.promises.stat(filePath);
const ext = path.extname(filePath).toLowerCase();
res.statusCode = 200;
res.setHeader('Content-Type', MIME_TYPES[ext] ?? 'application/octet-stream');
res.setHeader('Content-Length', String(stat.size));
res.setHeader('Cache-Control', 'no-cache');
if (method === 'HEAD') {
res.end();
return;
}
const stream = fs.createReadStream(filePath);
await new Promise<void>((resolve, reject) => {
stream.once('error', reject);
res.once('error', reject);
res.once('finish', resolve);
stream.pipe(res);
});
}
function writeBody(
res: http.ServerResponse<http.IncomingMessage>,
method: string,
body: string,
): void {
res.setHeader('Content-Length', Buffer.byteLength(body));
if (method === 'HEAD') {
res.end();
return;
}
res.end(body);
}
function proxyWebSocketUpgrade(
req: http.IncomingMessage,
socket: Duplex,
head: Buffer,
targetOrigin: string,
): void {
let target: URL;
try {
target = new URL(targetOrigin);
} catch {
socket.write('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
socket.destroy();
return;
}
if (!['ws:', 'wss:', 'http:', 'https:'].includes(target.protocol)) {
socket.write('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
socket.destroy();
return;
}
const port = target.port
? Number(target.port)
: (target.protocol === 'wss:' || target.protocol === 'https:' ? 443 : 80);
const upstream: Duplex = (target.protocol === 'wss:' || target.protocol === 'https:'
? tls.connect(port, target.hostname, { servername: target.hostname })
: net.connect(port, target.hostname));
upstream.once('connect', () => {
const forwardedPath = buildProxyPath(target, req.url);
let rawRequest = `GET ${forwardedPath} HTTP/${req.httpVersion}\r\n`;
rawRequest += `Host: ${target.host}\r\n`;
for (const [key, value] of Object.entries(req.headers)) {
const lowerKey = key.toLowerCase();
if (lowerKey === 'host' || lowerKey === 'origin' || value === undefined) {
continue;
}
const values = Array.isArray(value) ? value : [value];
for (const headerValue of values) {
rawRequest += `${key}: ${headerValue}\r\n`;
}
}
rawRequest += '\r\n';
upstream.write(rawRequest);
if (head.length > 0) {
upstream.write(head);
}
socket.pipe(upstream);
upstream.pipe(socket);
});
upstream.once('error', () => {
socket.destroy();
});
socket.once('error', () => {
upstream.destroy();
});
}
function buildProxyPath(target: URL, rawUrl: string | undefined): string {
const incoming = new URL(rawUrl ?? '/', 'http://matrix.local');
const basePath = target.pathname === '/' ? '' : target.pathname.replace(/\/$/, '');
if (basePath.length > 0) {
return `${basePath}${incoming.search}`;
}
return `${incoming.pathname}${incoming.search}`;
}
function resolveBrandingDir(packageDir: string): string | null {
const brandingDir = path.join(packageDir, 'branding');
return fs.existsSync(path.join(brandingDir, 'manifest.json')) ? brandingDir : null;
}
function safeDecodePath(pathname: string): string | null {
try {
const decoded = decodeURIComponent(pathname);
if (decoded.includes('\0')) {
return null;
}
return decoded;
} catch {
return null;
}
}

View File

@ -0,0 +1,185 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
export type MatrixWorkspacePackageStore = 'global' | 'system';
export type MatrixWorkspaceServiceDeclaration =
| { readonly path: string }
| { readonly package: string; readonly store?: MatrixWorkspacePackageStore };
export interface MatrixWorkspaceConfig {
readonly home?: string;
readonly default?: readonly string[];
readonly services?: Record<string, MatrixWorkspaceServiceDeclaration>;
}
export interface MatrixWorkspaceInitResult {
readonly workspaceRoot: string;
readonly workspaceConfigPath: string;
readonly home: string;
readonly created: boolean;
}
export function ensureMatrixWorkspace(workspaceRoot: string): MatrixWorkspaceInitResult {
const root = path.resolve(workspaceRoot);
const matrixDir = path.join(root, '.matrix');
const workspaceConfigPath = path.join(matrixDir, 'workspace.json');
fs.mkdirSync(matrixDir, { recursive: true });
let created = false;
if (!fs.existsSync(workspaceConfigPath)) {
writeWorkspaceConfig(root, {
home: '.matrix/home',
default: [],
services: {},
});
created = true;
}
const config = readWorkspaceConfig(root);
const home = path.resolve(root, config.home ?? '.matrix/home');
fs.mkdirSync(home, { recursive: true });
return {
workspaceRoot: root,
workspaceConfigPath,
home,
created,
};
}
export function readWorkspaceConfig(workspaceRoot: string): MatrixWorkspaceConfig {
const workspaceConfigPath = path.join(path.resolve(workspaceRoot), '.matrix', 'workspace.json');
const parsed = JSON.parse(fs.readFileSync(workspaceConfigPath, 'utf8')) as unknown;
if (!isRecord(parsed)) {
throw new Error(`${workspaceConfigPath} must contain a JSON object`);
}
return {
...(typeof parsed.home === 'string' && parsed.home.trim() ? { home: parsed.home.trim() } : {}),
...(Array.isArray(parsed.default) ? { default: parsed.default.filter(isNonEmptyString) } : {}),
...(isRecord(parsed.services) ? { services: normalizeServices(parsed.services, workspaceConfigPath) } : {}),
};
}
export function writeWorkspaceConfig(workspaceRoot: string, config: MatrixWorkspaceConfig): string {
const root = path.resolve(workspaceRoot);
const matrixDir = path.join(root, '.matrix');
const workspaceConfigPath = path.join(matrixDir, 'workspace.json');
fs.mkdirSync(matrixDir, { recursive: true });
fs.writeFileSync(workspaceConfigPath, `${JSON.stringify({
home: config.home ?? '.matrix/home',
default: config.default ?? [],
services: config.services ?? {},
}, null, 2)}\n`, 'utf8');
return workspaceConfigPath;
}
export function addWorkspaceService(
workspaceRoot: string,
serviceId: string,
declaration: MatrixWorkspaceServiceDeclaration,
options: { readonly addToDefault?: boolean } = {},
): string {
ensureMatrixWorkspace(workspaceRoot);
const config = readWorkspaceConfig(workspaceRoot);
const services = { ...(config.services ?? {}) };
services[serviceId] = declaration;
const currentDefault = config.default ?? [];
const nextDefault = options.addToDefault === false || currentDefault.includes(serviceId)
? currentDefault
: [...currentDefault, serviceId];
return writeWorkspaceConfig(workspaceRoot, {
...config,
services,
default: nextDefault,
});
}
export function ensureBaseHostServices(workspaceRoot: string): string {
ensureMatrixWorkspace(workspaceRoot);
const config = readWorkspaceConfig(workspaceRoot);
const services = { ...(config.services ?? {}) };
const systemSource = findSourceCheckoutPackage(workspaceRoot, 'system');
const hostControlSource = findSourceCheckoutPackage(workspaceRoot, 'host-control');
if (!services.system) {
services.system = systemSource
? { path: relativeWorkspacePath(workspaceRoot, systemSource) }
: { package: '@open-matrix/system', store: 'system' };
}
if (!services['host-control']) {
services['host-control'] = hostControlSource
? { path: relativeWorkspacePath(workspaceRoot, hostControlSource) }
: { package: '@open-matrix/host-control', store: 'system' };
}
const currentDefault = config.default ?? [];
const prefix = ['system', 'host-control'];
return writeWorkspaceConfig(workspaceRoot, {
...config,
services,
default: [
...prefix,
...currentDefault.filter((entry) => !prefix.includes(entry)),
],
});
}
export function findSourceCheckoutPackage(workspaceRoot: string, packageDirName: string): string | null {
const root = path.resolve(workspaceRoot);
const candidates = [
path.join(root, 'projects', 'matrix-3', 'packages', packageDirName),
path.join(root, 'packages', packageDirName),
];
for (const candidate of candidates) {
if (
fs.existsSync(path.join(candidate, 'package.json'))
&& fs.existsSync(path.join(candidate, 'matrix.json'))
) {
return candidate;
}
}
return null;
}
export function relativeWorkspacePath(workspaceRoot: string, target: string): string {
const relative = path.relative(path.resolve(workspaceRoot), path.resolve(target)).replaceAll(path.sep, '/');
if (!relative) {
return '.';
}
return relative.startsWith('.') ? relative : `./${relative}`;
}
function normalizeServices(
services: Record<string, unknown>,
workspaceConfigPath: string,
): Record<string, MatrixWorkspaceServiceDeclaration> {
const normalized: Record<string, MatrixWorkspaceServiceDeclaration> = {};
for (const [id, value] of Object.entries(services)) {
if (!isRecord(value)) {
throw new Error(`${workspaceConfigPath}: services.${id} must be an object`);
}
if (typeof value.path === 'string' && value.path.trim()) {
normalized[id] = { path: value.path.trim() };
continue;
}
if (typeof value.package === 'string' && value.package.trim()) {
const store = value.store === 'system' ? 'system' : value.store === 'global' ? 'global' : undefined;
normalized[id] = {
package: value.package.trim(),
...(store ? { store } : {}),
};
continue;
}
throw new Error(`${workspaceConfigPath}: services.${id} must declare path or package`);
}
return normalized;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}

View File

@ -0,0 +1,171 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { actorCommand } from '../../src/commands/actor.js';
import { installOpenMatrixCoreStub } from '../support/installCoreStub.ts';
import { loadMatrixServiceManifest } from '../../src/utils/service-manifest.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../../../');
describe('mx actor command', () => {
it('creates a compilable actor scaffold package', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-'));
const outputDir = path.join(tmpRoot, 'my-weather-service');
try {
const result = await actorCommand('my-weather-service', {
cwd: tmpRoot,
outputDir,
dev: true,
});
assert.equal(result.rootDir, outputDir);
assert.equal(result.workspacePath, path.join(tmpRoot, '.matrix', 'workspace.json'));
assert.equal(fs.existsSync(path.join(outputDir, 'matrix.json')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.className}.ts`)), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', 'index.ts')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'get-status.mxq')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'skills', 'my-weather-service.skill.md')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'tests', 'my-weather-service.spec.ts')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'package.json')), true, 'package.json should be generated');
assert.equal(fs.existsSync(path.join(outputDir, 'tsconfig.json')), true, 'tsconfig.json should be generated');
assert.equal(fs.existsSync(path.join(outputDir, 'matrix.service.json')), false, 'standalone factory is declared in matrix.json');
assert.equal(
fs.existsSync(path.join(outputDir, 'src', `create-standalone-${result.className}.ts`)),
true,
'standalone service factory should be generated',
);
const pkg = JSON.parse(fs.readFileSync(path.join(outputDir, 'package.json'), 'utf8'));
assert.equal(pkg.type, 'module', 'package.json should have type: module');
assert.ok(Array.isArray(pkg.files), 'package.json should have files array');
assert.equal(pkg.files.includes('matrix.service.json'), false, 'package files should not ship a second service manifest');
assert.equal(pkg.scripts?.build, 'tsc -p tsconfig.json');
assert.equal(pkg.scripts?.['build:watch'], 'tsc -w -p tsconfig.json --preserveWatchOutput');
assert.equal(pkg.scripts?.dev, 'tsc -w -p tsconfig.json --preserveWatchOutput');
assert.equal(pkg.scripts?.test, 'node --import tsx --test tests/**/*.spec.ts');
assert.equal(pkg.dependencies?.['@open-matrix/core'], '*', 'actor service package should declare runtime SDK dependency');
assert.equal(pkg.devDependencies?.['@open-matrix/core'], undefined, 'actor service package should not hide runtime SDK in devDependencies');
const tsconfig = JSON.parse(fs.readFileSync(path.join(outputDir, 'tsconfig.json'), 'utf8'));
assert.ok(tsconfig.compilerOptions, 'tsconfig.json should have compilerOptions');
assert.equal(tsconfig.compilerOptions.target, 'ES2022');
assert.equal(tsconfig.compilerOptions.incremental, true);
assert.equal(tsconfig.compilerOptions.tsBuildInfoFile, '.tsbuildinfo');
assert.equal(tsconfig.compilerOptions.sourceMap, true);
const manifest = JSON.parse(fs.readFileSync(path.join(outputDir, 'matrix.json'), 'utf8')) as {
name: string;
runtime?: {
factory?: {
kind?: string;
export?: string;
instance?: {
rootKind?: string;
componentType?: string;
autoStart?: boolean;
};
};
};
components: Array<{ mount?: string; type?: string }>;
};
assert.equal(manifest.name, '@matrix/my-weather-service');
assert.equal(manifest.components[0]?.mount, 'my-weather-service');
assert.equal(manifest.components[0]?.type, result.className);
assert.equal(manifest.runtime?.factory?.kind, 'factory');
assert.equal(manifest.runtime?.factory?.export, `createStandalone${result.className}`);
assert.equal(manifest.runtime?.factory?.instance?.rootKind, 'component');
assert.equal(manifest.runtime?.factory?.instance?.componentType, result.className);
assert.equal(manifest.runtime?.factory?.instance?.autoStart, true);
// Install a minimal runtime stub so the scaffolded actor can resolve
// the published package import shape without depending on a built workspace copy.
installOpenMatrixCoreStub(outputDir);
const compile = spawnSync(
process.execPath,
[
path.resolve(repoRoot, 'node_modules/tsx/dist/cli.mjs'),
path.join(outputDir, 'src', `${result.className}.ts`),
],
{ cwd: outputDir, encoding: 'utf8' }
);
assert.equal(
compile.status,
0,
`expected generated actor to compile, stderr: ${compile.stderr || '(none)'}`
);
const build = spawnSync(
process.execPath,
[
path.resolve(repoRoot, 'node_modules/typescript/bin/tsc'),
'-p',
path.join(outputDir, 'tsconfig.json'),
],
{ cwd: outputDir, encoding: 'utf8' },
);
assert.equal(build.status, 0, `expected generated package to build, stderr: ${build.stderr || '(none)'}`);
const loadedService = loadMatrixServiceManifest(outputDir);
assert.equal(loadedService.manifestPath, path.join(outputDir, 'matrix.json'));
assert.equal(loadedService.exportName, `createStandalone${result.className}`);
assert.equal(loadedService.entryPath, path.join(outputDir, 'dist', 'index.js'));
assert.equal(loadedService.serviceInstance.rootKind, 'component');
assert.equal(loadedService.serviceInstance.mount, 'my-weather-service');
const workspace = JSON.parse(fs.readFileSync(result.workspacePath!, 'utf8'));
assert.equal(workspace.home, '.matrix/home');
assert.deepEqual(workspace.default, ['system', 'host-control', 'my-weather-service']);
assert.deepEqual(workspace.services?.system, {
package: '@open-matrix/system',
store: 'system',
});
assert.deepEqual(workspace.services?.['host-control'], {
package: '@open-matrix/host-control',
store: 'system',
});
assert.deepEqual(workspace.services?.['my-weather-service'], {
path: './my-weather-service',
});
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
it('adds the source checkout system service before the actor in dev workspaces', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-system-dev-'));
const systemDir = path.join(tmpRoot, 'projects', 'matrix-3', 'packages', 'system');
const hostControlDir = path.join(tmpRoot, 'projects', 'matrix-3', 'packages', 'host-control');
try {
fs.mkdirSync(systemDir, { recursive: true });
fs.writeFileSync(path.join(systemDir, 'package.json'), JSON.stringify({ name: '@open-matrix/system' }), 'utf8');
fs.writeFileSync(path.join(systemDir, 'matrix.json'), JSON.stringify({ name: '@open-matrix/system' }), 'utf8');
fs.mkdirSync(hostControlDir, { recursive: true });
fs.writeFileSync(path.join(hostControlDir, 'package.json'), JSON.stringify({ name: '@open-matrix/host-control' }), 'utf8');
fs.writeFileSync(path.join(hostControlDir, 'matrix.json'), JSON.stringify({ name: '@open-matrix/host-control' }), 'utf8');
const result = await actorCommand('dev-loop-demo', {
cwd: tmpRoot,
outputDir: path.join(tmpRoot, 'dev-loop-demo'),
dev: true,
});
const workspace = JSON.parse(fs.readFileSync(result.workspacePath!, 'utf8'));
assert.equal(workspace.home, '.matrix/home');
assert.deepEqual(workspace.default, ['system', 'host-control', 'dev-loop-demo']);
assert.deepEqual(workspace.services?.system, { path: './projects/matrix-3/packages/system' });
assert.deepEqual(workspace.services?.['host-control'], { path: './projects/matrix-3/packages/host-control' });
assert.deepEqual(workspace.services?.['dev-loop-demo'], { path: './dev-loop-demo' });
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
});

View File

@ -0,0 +1,408 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawn } from 'node:child_process';
import { resolveNatsBinary } from '../../src/utils/runner-transport.js';
import { assertExitCode, parseLastJson, runMxCli } from '../helpers/cli-harness.js';
interface IWebSocketLike {
addEventListener(
type: 'open' | 'error' | 'close',
listener: (event: unknown) => void,
options?: { once?: boolean },
): void;
close(): void;
}
async function getAvailablePorts(count: number): Promise<number[]> {
const servers = await Promise.all(
Array.from({ length: count }, () => new Promise<net.Server>((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => resolve(server));
})),
);
const ports = servers.map((server) => {
const address = server.address();
assert.ok(address && typeof address === 'object');
return address.port;
});
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
})));
return ports;
}
async function createRunInPlaceFixture(rootDir: string): Promise<string> {
const packageDir = path.join(rootDir, 'sample-run-package');
const runtimeDir = path.join(packageDir, 'dist', 'runtime');
fs.mkdirSync(runtimeDir, { recursive: true });
fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
const [natsPort, wsPort] = await getAvailablePorts(2);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify({
name: '@acme/sample-run-package',
version: '1.0.0',
type: 'module',
main: './dist/runtime/index.js',
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify({
name: '@acme/sample-run-package',
version: '1.0.0',
runtime: {
language: 'typescript',
entry: './dist/runtime/index.js',
factory: {
kind: 'factory',
export: 'createSampleRunService',
bootstrap: {
overrides: {
mode: 'run-in-place',
},
},
instance: {
packageName: '@acme/sample-run-package',
packageRevision: '1.0.0',
class: 'service',
rootKind: 'package-root',
mount: 'sample-run.local',
autoStart: true,
},
},
},
components: [
{
type: 'SampleRunService',
export: 'SampleRunService',
mount: 'sample-run',
autoStart: true,
},
],
permissions: {
fsPolicy: 'none',
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, '.matrix', 'dev.environment.json'),
JSON.stringify({
name: 'dev',
runtime: {
root: 'COM.TEST.RUNNER',
runtimeId: 'sample-run-dev',
},
nats: {
mode: 'embedded',
port: natsPort,
wsPort,
binaryPath: resolveNatsBinary(process.cwd()),
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(runtimeDir, 'index.js'),
`export async function createSampleRunService(overrides = {}, _services, _configResolution, bootstrapContext) {
return {
ok: true,
overrides,
bootContext: bootstrapContext,
runtime: {
async shutdown() {
return undefined;
}
}
};
}
`,
'utf8',
);
return packageDir;
}
async function createServeFixture(rootDir: string): Promise<{
packageDir: string;
httpPort: number;
}> {
const packageDir = path.join(rootDir, 'sample-serve-package');
const distDir = path.join(packageDir, 'dist');
fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
fs.mkdirSync(distDir, { recursive: true });
fs.mkdirSync(path.join(packageDir, 'branding'), { recursive: true });
const [httpPort, natsPort, wsPort] = await getAvailablePorts(3);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify({
name: '@acme/sample-serve-package',
version: '1.0.0',
type: 'module',
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify({
name: '@acme/sample-serve-package',
version: '1.0.0',
webapp: {
distDir: 'dist',
entry: 'index.html',
appName: 'sample-serve',
},
permissions: {
fsPolicy: 'none',
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, '.matrix', 'dev.environment.json'),
JSON.stringify({
name: 'dev',
runtime: {
root: 'COM.TEST.SERVE',
runtimeId: 'sample-serve-dev',
},
nats: {
mode: 'embedded',
port: natsPort,
wsPort,
binaryPath: resolveNatsBinary(process.cwd()),
},
http: {
enabled: true,
port: httpPort,
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(distDir, 'index.html'),
'<!DOCTYPE html><html><head><title>Sample Serve</title><link rel="manifest" href="/manifest.json"></head><body><div id="app">Sample Serve Fixture</div></body></html>\n',
'utf8',
);
fs.writeFileSync(
path.join(distDir, 'app.js'),
'console.log("sample-serve");\n',
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'branding', 'manifest.json'),
JSON.stringify({
name: 'Sample Serve',
short_name: 'Sample',
start_url: '/',
display: 'standalone',
icons: [{ src: '/branding/favicon.svg', sizes: '16x16', type: 'image/svg+xml' }],
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'branding', 'favicon.svg'),
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" fill="#111827"/><path d="M4 11h8v2H4zM5 3h6v2H5zm1 4h4v2H6z" fill="#f8fafc"/></svg>\n',
'utf8',
);
return { packageDir, httpPort };
}
async function waitForHttpReady(
url: string,
child: ReturnType<typeof spawn>,
timeoutMs = 15_000,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`mx run exited early with code ${child.exitCode}`);
}
try {
const response = await fetch(url);
if (response.ok) {
return;
}
} catch {
// Retry until the deadline expires.
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`Timed out waiting for ${url}`);
}
describe('mx run package front door', () => {
it('runs a package from the current folder via matrix.json runtime.factory and package-local environment', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-package-'));
const packageDir = await createRunInPlaceFixture(tempRoot);
assert.equal(fs.existsSync(path.join(packageDir, '.matrix', 'host.status.json')), false);
const result = await runMxCli(
['node', 'mx', 'run', '.', '--env', 'dev', '--check', '--json'],
{ cwd: packageDir },
);
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as Record<string, unknown>;
assert.equal(payload.packageName, '@acme/sample-run-package');
assert.equal(payload.packageDir, packageDir);
assert.equal(payload.environmentName, 'dev');
assert.equal(payload.root, 'COM.TEST.RUNNER');
assert.equal(payload.mount, 'sample-run.local');
assert.equal(payload.entryPath, path.join(packageDir, 'dist', 'runtime', 'index.js'));
assert.equal(payload.runnerTransport?.mode, 'embedded');
assert.equal(payload.runnerTransport?.root, 'COM.TEST.RUNNER');
assert.match(String(payload.runnerTransport?.url), /^nats:\/\/127\.0\.0\.1:\d+$/);
assert.match(String(payload.runnerTransport?.wsUrl), /^ws:\/\/127\.0\.0\.1:\d+$/);
});
it('fails clearly when a directory target does not declare a runtime factory', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-missing-service-'));
const packageDir = path.join(tempRoot, 'missing-service');
fs.mkdirSync(packageDir, { recursive: true });
const result = await runMxCli(
['node', 'mx', 'run', '.'],
{ cwd: packageDir },
);
assertExitCode(result, 1);
assert.match(
result.errors.join('\n'),
/does not declare a matrix\.json runtime\.factory/,
);
});
it('runs a webapp package as a Matrix asset runtime without a package HTTP server', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-webapp-assets-'));
const { packageDir } = await createServeFixture(tempRoot);
const result = await runMxCli(
['node', 'mx', 'run', '.', '--env', 'dev', '--check', '--json'],
{ cwd: packageDir },
);
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as Record<string, unknown>;
assert.equal(payload.packageName, '@acme/sample-serve-package');
assert.equal(payload.mode, 'webapp-assets');
assert.equal(payload.assetMount, 'system.runtimes.sample-serve-dev.http');
assert.equal(payload.baseUrl, undefined);
assert.equal(payload.runnerTransport?.mode, 'embedded');
});
it('publishes a served webapp HTTP origin in run metadata', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-webapp-origin-'));
const { packageDir, httpPort } = await createServeFixture(tempRoot);
const result = await runMxCli(
['node', 'mx', 'run', '.', '--env', 'dev', '--serve', '--check', '--json'],
{ cwd: packageDir },
);
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as Record<string, unknown>;
assert.equal(payload.packageName, '@acme/sample-serve-package');
assert.equal(payload.mode, 'standalone-webapp');
assert.equal(payload.assetMount, 'system.runtimes.sample-serve-dev.http');
assert.equal(payload.baseUrl, `http://127.0.0.1:${httpPort}`);
assert.equal(payload.httpPort, httpPort);
assert.equal(payload.runnerTransport?.mode, 'embedded');
});
it('serves built webapp assets with same-origin bootstrap and nats-ws routes', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-serve-'));
const { packageDir, httpPort } = await createServeFixture(tempRoot);
const mxCliDir = path.resolve(import.meta.dirname, '../..');
const cliEntry = path.resolve(import.meta.dirname, '../../src/index.ts');
const tsxLoader = path.resolve(import.meta.dirname, '../../../../node_modules/tsx/dist/loader.mjs');
const child = spawn(
process.execPath,
['--import', tsxLoader, cliEntry, 'run', packageDir, '--env', 'dev', '--serve'],
{
cwd: mxCliDir,
stdio: ['ignore', 'pipe', 'pipe'],
},
);
const stdout: string[] = [];
const stderr: string[] = [];
child.stdout.on('data', (chunk: Buffer) => {
stdout.push(chunk.toString('utf8'));
});
child.stderr.on('data', (chunk: Buffer) => {
stderr.push(chunk.toString('utf8'));
});
try {
const baseUrl = `http://127.0.0.1:${httpPort}`;
await waitForHttpReady(`${baseUrl}/healthz`, child);
const html = await fetch(`${baseUrl}/`);
assert.equal(html.status, 200);
assert.match(await html.text(), /Sample Serve Fixture/);
const bootstrap = await fetch(`${baseUrl}/api/bootstrap`);
assert.equal(bootstrap.status, 200);
const payload = await bootstrap.json() as {
root?: string;
transportPlan?: { wsPath?: string; authMode?: string; sessionMode?: string };
};
assert.equal(payload.root, 'COM.TEST.SERVE');
assert.equal(payload.transportPlan?.wsPath, '/nats-ws');
assert.equal(payload.transportPlan?.authMode, 'anonymous');
assert.equal(payload.transportPlan?.sessionMode, 'local-client');
const manifest = await fetch(`${baseUrl}/manifest.json`);
assert.equal(manifest.status, 200);
const branding = await fetch(`${baseUrl}/branding/favicon.svg`);
assert.equal(branding.status, 200);
const WebSocketCtor = (globalThis as typeof globalThis & {
WebSocket?: new (url: string) => IWebSocketLike;
}).WebSocket;
assert.ok(WebSocketCtor, 'Node runtime must expose WebSocket for same-origin proxy proof');
const ws = new WebSocketCtor(`ws://127.0.0.1:${httpPort}/nats-ws`);
await new Promise<void>((resolve, reject) => {
ws.addEventListener('open', () => {
ws.close();
}, { once: true });
ws.addEventListener('error', (event) => {
reject(new Error(`WebSocket upgrade failed: ${String(event)}`));
}, { once: true });
ws.addEventListener('close', () => {
resolve();
}, { once: true });
});
} finally {
child.kill('SIGTERM');
await new Promise<void>((resolve) => {
child.once('exit', () => resolve());
});
}
assert.match(stdout.join(''), /serving @acme\/sample-serve-package/);
assert.doesNotMatch(stderr.join(''), /Run target not found|does not declare|Error:/);
});
});

View File

@ -0,0 +1,920 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
InMemoryBroker,
InMemoryTransport,
MatrixActor,
MatrixRuntime,
RequestReply,
subscribeRuntimePresence,
} from '@open-matrix/core';
import { decideMatrixPlacementBind } from '@open-matrix/contracts/placement';
import {
heartbeatRunnerRuntime,
registerRunnerBindings,
registerRunnerRuntime,
shouldRegisterRuntimeInventoryClaim,
} from '../../src/utils/runner-control-plane.js';
import { ServiceRegistryActor } from '@open-matrix/system-platform';
import { BindingsRegistryActor } from '@open-matrix/system-bindings';
import { installRunnerPlacementPolicy } from '../../src/utils/runner-placement-policy.js';
import type { ILoadedMatrixServiceManifest } from '../../src/utils/service-manifest.js';
const runtimePresenceManifest: ILoadedMatrixServiceManifest = {
packageDir: '/tmp/runtime-presence-package',
packageName: '@open-matrix/runtime-presence-test',
packageNamespace: 'runtime-presence-test',
manifestPath: '/tmp/runtime-presence-package/matrix.json',
entryPath: '/tmp/runtime-presence-package/dist/index.js',
exportName: 'RuntimePresenceTestActor',
kind: 'factory',
overrides: {},
root: 'SPACE-RUNNER',
mount: 'runtime-presence-test',
packageRoot: {
mount: 'runtime-presence-test',
accepts: ['runtime-presence.ping'],
},
packageComponents: [],
serviceInstance: {
id: 'runtime-presence-runner',
packageName: '@open-matrix/runtime-presence-test',
class: 'RuntimePresenceTestActor',
rootKind: 'package-root',
autoStart: true,
registrationSource: 'matrix-service',
},
};
class PlacementGatewayActor extends MatrixActor {
static override description = 'Placement gateway test actor';
}
class PlacementCredentialBridgeActor extends MatrixActor {
static override description = 'Placement credential bridge test actor';
}
class FakeAuthorityActor extends MatrixActor {
static override accepts = {
'fake.ping': {},
};
onFakePing(): { ok: true; actor: 'authority' } {
return { ok: true, actor: 'authority' };
}
}
class FakeDeviceActor extends MatrixActor {
static override accepts = {
'fake.ping': {},
};
onFakePing(): { ok: true; actor: 'device' } {
return { ok: true, actor: 'device' };
}
}
describe('runner control plane', () => {
it('keeps ephemeral runtime mounts out of the logical registry', () => {
assert.equal(
shouldRegisterRuntimeInventoryClaim('RUNTIME-HOST-CHAT', 'system.runtimes.RUNTIME-HOST-CHAT'),
false,
);
assert.equal(
shouldRegisterRuntimeInventoryClaim('RUNTIME-HOST-CHAT', 'system.runtimes.RUNTIME-HOST-CHAT.control'),
false,
);
assert.equal(
shouldRegisterRuntimeInventoryClaim('RUNTIME-BROWSER-1234', 'system.runtimes.RUNTIME-BROWSER-1234.director'),
false,
);
});
it('continues to register canonical actor mounts', () => {
assert.equal(shouldRegisterRuntimeInventoryClaim('RUNTIME-HOST-CHAT', 'chat'), true);
assert.equal(shouldRegisterRuntimeInventoryClaim('RUNTIME-HOST-CHAT', 'chat.conversation'), true);
assert.equal(shouldRegisterRuntimeInventoryClaim('RUNTIME-HOST-SYSTEM', 'system.runtimes'), true);
});
it('publishes runtime presence when registering a runner runtime', async () => {
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-presence-test',
root: 'SPACE-RUNNER',
}),
logging: false,
});
const observed: string[] = [];
const unsubscribe = subscribeRuntimePresence(
runtime.getRootContext(),
'SPACE-RUNNER',
(record, event) => {
observed.push(`${event}:${record.runtimeId}:${record.app ?? ''}:${record.health?.status ?? ''}`);
},
);
try {
await registerRunnerRuntime(
runtime,
runtimePresenceManifest,
undefined,
undefined,
'runtime-presence-test',
);
// Slice 1b: runtime directory authority is system.runtimes
// (RuntimeManagerActor). registerRunnerRuntime publishes presence on the
// bus; subscribers like RuntimeManagerActor observe and serve it via
// runtimes.list. There is no longer a `kind: 'runtime'` registry claim.
assert.deepEqual(observed, ['announce:runtime-presence-runner:@open-matrix/runtime-presence-test:ok']);
} finally {
unsubscribe();
await runtime.shutdown();
}
});
it('marks linked-device control-plane calls for broker protocol request replies', async () => {
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-linked-authority-test',
root: 'test-user-1',
}),
logging: false,
});
try {
const registration = await registerRunnerRuntime(
runtime,
runtimePresenceManifest,
{
packageDir: '/tmp/runtime-presence-package',
envName: 'hivecast',
environmentPath: '/tmp/runtime-presence-package/.matrix/hivecast.environment.json',
environment: {
name: 'hivecast',
runtime: {
root: 'test-user-1',
runtimeId: 'runtime-presence-runner',
},
nats: {
mode: 'external',
url: 'nats://dev-platform:4222',
credentialsRef: 'file:/var/lib/hivecast/credentials/hivecast-nats.json',
},
host: {
matrixDir: '/var/lib/hivecast',
},
},
},
{
envName: 'hivecast',
root: 'test-user-1',
mode: 'external',
url: 'nats://dev-platform:4222',
credentialsRef: 'file:/var/lib/hivecast/credentials/hivecast-nats.json',
},
'runtime-presence-test',
);
assert.equal(registration.controlTargetRoot, 'test-user-1');
assert.equal(registration.protocolRequest, true);
} finally {
await runtime.shutdown();
}
});
it('reclaim re-issues registry.claim after the registry actor was wiped (simulates SYSTEM restart)', async () => {
// This test pins the production failure mode that motivated the reclaim()
// closure on IRunnerRuntimeRegistration / IRunnerBindingRegistration:
//
// 1. Runner registers, registry stores claim in its in-memory _claims Map
// 2. SYSTEM runtime restarts (deploy, supervisor bounce). Its _claims
// starts empty
// 3. Runner's 10s heartbeat calls registry.claim.renew → registry has
// no record → returns { renewed: 0 }
// 4. Without reclaim, the runner's original 45s-TTL claim expires and
// the runtime vanishes from registry.query
//
// The fix: heartbeatRunnerRuntime / renewRunnerBindings return
// { known: false } / { refreshed: 0 } when the registry forgot us, and
// the heartbeat tick calls registration.reclaim() to re-issue claims.
const home = mkdtempSync(join(tmpdir(), 'runner-reclaim-test-'));
writeFileSync(join(home, 'matrix.json'), JSON.stringify({
name: '@open-matrix/reclaim-test',
root: {
type: 'ReclaimTestRootActor',
mount: 'reclaim-test',
},
components: [],
}));
const reclaimManifest: ILoadedMatrixServiceManifest = {
...runtimePresenceManifest,
packageDir: home,
packageName: '@open-matrix/reclaim-test',
packageNamespace: 'reclaim-test',
manifestPath: join(home, 'matrix.json'),
entryPath: join(home, 'dist/index.js'),
packageRoot: {
mount: 'reclaim-test',
accepts: ['reclaim.ping'],
},
serviceInstance: {
...runtimePresenceManifest.serviceInstance,
id: 'reclaim-test-runner',
packageName: '@open-matrix/reclaim-test',
},
};
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-reclaim-test',
root: 'SPACE-RECLAIM',
}),
logging: false,
});
const queryRegistry = async () => await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ kind: 'actor', includeStale: true },
{ timeoutMs: 5_000 },
) as { entries?: Array<{ mount?: string; providerRuntimeId?: string }> };
const queryBindings = async () => await RequestReply.execute(
runtime.getRootContext(),
'system.bindings',
'bindings.list',
{},
{ timeoutMs: 5_000 },
) as { ok?: boolean; count?: number; bindings?: Array<{ binding?: string }> };
try {
const runtimeRegistration = await registerRunnerRuntime(
runtime,
reclaimManifest,
undefined,
undefined,
'reclaim-test',
);
const bindingRegistration = await registerRunnerBindings(
runtime,
reclaimManifest,
undefined,
runtimeRegistration.runtimeId,
'reclaim-test',
);
// Sanity: both registries know about us before wipe.
const beforeWipeRegistry = await queryRegistry();
const beforeRegistryMounts = new Set((beforeWipeRegistry.entries ?? []).map((e) => e.mount));
assert.ok(beforeRegistryMounts.size > 0, 'system.registry should have at least one claim before wipe');
const beforeWipeBindings = await queryBindings();
const beforeBindingMounts = new Set((beforeWipeBindings.bindings ?? []).map((b) => b.binding));
assert.ok(beforeBindingMounts.size > 0, 'system.bindings should have at least one binding before wipe');
// Simulate SYSTEM restart: tear down BOTH registries and replace them
// with fresh ones. Their _claims / _bindings maps start empty —
// exactly the state the production registries are in right after a
// SYSTEM bounce.
await runtime.remove('system.registry');
await runtime.remove('system.bindings');
await runtime.createSupervised(ServiceRegistryActor, 'system.registry');
await runtime.createSupervised(BindingsRegistryActor, 'system.bindings');
const afterWipeRegistry = await queryRegistry();
assert.deepEqual(
afterWipeRegistry.entries ?? [],
[],
'system.registry should be empty after wipe (production failure mode)',
);
const afterWipeBindings = await queryBindings();
assert.deepEqual(
afterWipeBindings.bindings ?? [],
[],
'system.bindings should be empty after wipe (production failure mode)',
);
// Reclaim should restore every server-side record we published at
// registration time. After both calls the registries see us again.
const runtimeReclaim = await runtimeRegistration.reclaim();
assert.ok(
runtimeReclaim.inventoryClaimCount >= 0,
'runtime reclaim returns the count of inventory claims re-issued',
);
const bindingReclaim = await bindingRegistration.reclaim();
assert.ok(
bindingReclaim.registryClaimCount >= 1,
'binding reclaim re-issues at least the canonical package-root registry claim',
);
assert.ok(
bindingReclaim.bindingsRegisteredCount >= 1,
'binding reclaim re-issues at least the canonical package-root bindings.register',
);
assert.equal(
bindingReclaim.registryClaimCount,
bindingReclaim.bindingsRegisteredCount,
'every canonical binding produces exactly one registry.claim AND one bindings.register; reclaim must restore them in lockstep',
);
const afterReclaimRegistry = await queryRegistry();
const afterReclaimRegistryMounts = new Set((afterReclaimRegistry.entries ?? []).map((e) => e.mount));
assert.ok(
afterReclaimRegistryMounts.size >= beforeRegistryMounts.size,
`reclaim should restore all registry claims (had ${beforeRegistryMounts.size}, now ${afterReclaimRegistryMounts.size})`,
);
assert.ok(
afterReclaimRegistryMounts.has('reclaim-test'),
'canonical package-root registry claim should be reclaimed',
);
const afterReclaimBindings = await queryBindings();
const afterReclaimBindingMounts = new Set((afterReclaimBindings.bindings ?? []).map((b) => b.binding));
assert.deepEqual(
afterReclaimBindingMounts,
beforeBindingMounts,
'reclaim should restore every bindings.register record (otherwise canonical lookups break silently)',
);
} finally {
await runtime.shutdown();
rmSync(home, { recursive: true, force: true });
}
});
it('publishes placement metadata for runtime inventory and registry claims', async () => {
const home = mkdtempSync(join(tmpdir(), 'runner-placement-manifest-'));
const manifestPath = join(home, 'matrix.json');
writeFileSync(manifestPath, JSON.stringify({
name: '@open-matrix/placement-test',
root: {
type: 'PlacementRootActor',
mount: 'system',
},
components: [
{
type: 'PlacementRegistryActor',
mount: 'system.registry',
placement: { cardinality: 'authority-singleton' },
},
{
type: 'PlacementGatewayActor',
mount: 'system.gateway',
placement: { cardinality: 'device-local-control' },
},
{
type: 'PlacementCredentialBridgeActor',
mount: 'system.security.credential-bridge',
placement: { cardinality: 'device-singleton' },
},
],
}));
const manifest: ILoadedMatrixServiceManifest = {
...runtimePresenceManifest,
packageDir: home,
packageName: '@open-matrix/placement-test',
packageNamespace: 'system',
manifestPath,
entryPath: join(home, 'dist/index.js'),
serviceInstance: {
...runtimePresenceManifest.serviceInstance,
id: 'placement-test-runner',
packageName: '@open-matrix/placement-test',
},
};
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-placement-test',
root: 'SPACE-RUNNER',
}),
logging: false,
});
try {
await runtime.createSupervised(PlacementGatewayActor, 'system.gateway');
await runtime.createSupervised(PlacementCredentialBridgeActor, 'system.security.credential-bridge');
const registration = await registerRunnerRuntime(
runtime,
manifest,
undefined,
undefined,
'system',
);
const inventoryByMount = new Map(
(registration.presence.inventory ?? []).map((entry) => [entry.mount, entry]),
);
assert.equal(inventoryByMount.get('system.registry')?.placement?.cardinality, 'authority-singleton');
assert.equal(inventoryByMount.get('system.registry')?.placement?.claimMode, 'authoritative');
assert.equal(inventoryByMount.get('system.registry')?.placement?.logicalMount, 'system.registry');
assert.equal(inventoryByMount.get('system.registry')?.placement?.physicalMount, 'system.registry');
assert.equal(inventoryByMount.get('system.registry')?.placement?.callable, true);
assert.equal(inventoryByMount.get('system.gateway')?.placement?.cardinality, 'device-local-control');
assert.equal(inventoryByMount.get('system.gateway')?.placement?.claimMode, 'local-only');
assert.equal(inventoryByMount.get('system.security.credential-bridge')?.placement?.cardinality, 'device-singleton');
assert.equal(inventoryByMount.get('system.security.credential-bridge')?.placement?.claimMode, 'local-only');
const actorEntries = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ namespaceRoot: 'SPACE-RUNNER', kind: 'actor' },
{ timeoutMs: 5_000 },
) as {
entries?: Array<{
mount?: string;
claim?: { mode?: string };
placement?: {
cardinality?: string;
claimMode?: string;
logicalMount?: string;
physicalMount?: string;
callable?: boolean;
};
metadata?: { placement?: { callable?: boolean } };
}>;
};
const byMount = new Map((actorEntries.entries ?? []).map((entry) => [entry.mount, entry]));
assert.equal(byMount.get('system.registry')?.claim?.mode, 'authoritative');
assert.equal(byMount.get('system.registry')?.placement?.cardinality, 'authority-singleton');
assert.equal(byMount.get('system.registry')?.placement?.claimMode, 'authoritative');
assert.equal(byMount.get('system.registry')?.placement?.callable, true);
assert.equal(byMount.get('system.registry')?.metadata?.placement?.callable, true);
assert.equal(byMount.get('system.gateway')?.claim?.mode, 'local-only');
assert.equal(byMount.get('system.gateway')?.placement?.cardinality, 'device-local-control');
assert.equal(byMount.get('system.gateway')?.placement?.callable, true);
assert.equal(byMount.get('system.security.credential-bridge')?.claim?.mode, 'local-only');
assert.equal(byMount.get('system.security.credential-bridge')?.placement?.cardinality, 'device-singleton');
} finally {
await runtime.shutdown();
rmSync(home, { recursive: true, force: true });
}
});
it('blocks manifest authority-singleton actors before inbox bind on standby topology', async () => {
const home = mkdtempSync(join(tmpdir(), 'runner-placement-denial-'));
writeFileSync(join(home, 'matrix.json'), JSON.stringify({
name: '@open-matrix/placement-denial-test',
components: [
{
type: 'FakeAuthorityActor',
mount: 'system.fakeAuthority',
placement: {
cardinality: 'authority-singleton',
},
},
{
type: 'FakeDeviceActor',
mount: 'system.fakeDevice',
placement: {
cardinality: 'device-local-control',
},
},
],
}));
const manifest: ILoadedMatrixServiceManifest = {
...runtimePresenceManifest,
packageDir: home,
packageName: '@open-matrix/placement-denial-test',
packageNamespace: 'system',
manifestPath: join(home, 'matrix.json'),
entryPath: join(home, 'dist/index.js'),
packageComponents: [
{
mount: 'system.fakeAuthority',
accepts: ['fake.ping'],
},
{
mount: 'system.fakeDevice',
accepts: ['fake.ping'],
},
],
serviceInstance: {
...runtimePresenceManifest.serviceInstance,
id: 'placement-denial-runner',
packageName: '@open-matrix/placement-denial-test',
},
};
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-placement-denial-test',
root: 'SPACE-HIVECAST-LAB',
}),
logging: false,
});
try {
installRunnerPlacementPolicy({
runtime,
manifest,
environment: {
packageDir: home,
envName: 'standby',
environmentPath: join(home, '.matrix', 'standby.environment.json'),
environment: {
name: 'standby',
runtime: {
root: 'SPACE-HIVECAST-LAB',
runtimeId: 'placement-denial-runner',
},
topology: {
kind: 'linked-device',
roles: {
authorityCoordinator: {
active: false,
reason: 'linked-device-standby',
},
},
authority: {
authorityRoot: 'SPACE-HIVECAST-LAB',
},
},
},
},
runtimeId: 'placement-denial-runner',
authorityRoot: 'SPACE-HIVECAST-LAB',
});
await assert.rejects(
() => runtime.createSupervised(FakeAuthorityActor, 'system.fakeAuthority'),
/SINGLETON_MOUNT_NOT_OWNED/,
);
await runtime.createSupervised(FakeDeviceActor, 'system.fakeDevice');
const registration = await registerRunnerRuntime(
runtime,
manifest,
undefined,
undefined,
'system',
);
const bindings = await registerRunnerBindings(
runtime,
manifest,
undefined,
registration.runtimeId,
'system',
);
assert.equal(runtime.hasComponent('system.fakeAuthority'), false);
assert.equal(runtime.hasComponent('system.fakeDevice'), true);
assert.equal(bindings.bindings.some((binding) => binding.localMount === 'system.fakeAuthority'), false);
assert.equal(bindings.bindings.some((binding) => binding.localMount === 'system.fakeDevice'), true);
await assert.rejects(
() => RequestReply.execute(
runtime.getRootContext(),
'system.fakeAuthority',
'fake.ping',
{},
{ timeoutMs: 150 },
),
);
const deviceReply = await RequestReply.execute(
runtime.getRootContext(),
'system.fakeDevice',
'fake.ping',
{},
{ timeoutMs: 5_000 },
) as { ok?: boolean; actor?: string };
assert.deepEqual(deviceReply, { ok: true, actor: 'device' });
const inventoryByMount = new Map(
(registration.presence.inventory ?? []).map((entry) => [entry.mount, entry]),
);
assert.equal(inventoryByMount.get('system.fakeAuthority')?.placement?.cardinality, 'authority-singleton');
assert.equal(inventoryByMount.get('system.fakeAuthority')?.placement?.callable, false);
assert.equal(
inventoryByMount.get('system.fakeAuthority')?.placement?.standbyReason,
'SINGLETON_MOUNT_NOT_OWNED',
);
assert.equal(inventoryByMount.get('system.fakeDevice')?.placement?.cardinality, 'device-local-control');
assert.equal(inventoryByMount.get('system.fakeDevice')?.placement?.claimMode, 'local-only');
assert.equal(inventoryByMount.get('system.fakeDevice')?.placement?.callable, true);
// Slice 1b: query without namespaceRoot filter because
// registerRuntimeInventoryClaims writes claims with manifest.root as the
// namespaceRoot, while presence uses the transport root. The substrate
// truth here is "system.fakeDevice is claimed by this runtime" — the
// test does not need to assert which namespaceRoot the claim is stored
// under (that's a separate pre-existing inconsistency the workstream
// does not address).
const actorEntries = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ kind: 'actor', includeStale: true },
{ timeoutMs: 5_000 },
) as {
entries?: Array<{
mount?: string;
placement?: {
cardinality?: string;
claimMode?: string;
callable?: boolean;
};
}>;
};
const byMount = new Map((actorEntries.entries ?? []).map((entry) => [entry.mount, entry]));
assert.equal(byMount.has('system.fakeAuthority'), false);
assert.equal(byMount.get('system.fakeDevice')?.placement?.cardinality, 'device-local-control');
assert.equal(byMount.get('system.fakeDevice')?.placement?.claimMode, 'local-only');
assert.equal(byMount.get('system.fakeDevice')?.placement?.callable, true);
} finally {
await runtime.shutdown();
rmSync(home, { recursive: true, force: true });
}
});
it('uses authority leases to fence active authority-singleton placement', () => {
const activeLease = {
version: 1 as const,
authorityRoot: 'SPACE-HIVECAST-LAB',
leaseId: 'lease-active',
ownerNodeId: 'owner-a',
placementEpoch: 2,
acquiredAt: '2026-05-09T00:00:00.000Z',
heartbeatAt: '2026-05-09T00:00:00.000Z',
expiresAt: '2026-05-09T00:05:00.000Z',
status: 'active' as const,
scope: 'local-home' as const,
leaseAuthority: 'local' as const,
source: 'local-file' as const,
};
assert.equal(decideMatrixPlacementBind({
authorityRoot: 'SPACE-HIVECAST-LAB',
mount: 'system.registry',
cardinality: 'authority-singleton',
authorityCoordinatorActive: true,
ownerNodeId: 'owner-a',
authorityLease: activeLease,
}).allowed, true);
const nonOwned = decideMatrixPlacementBind({
authorityRoot: 'SPACE-HIVECAST-LAB',
mount: 'system.registry',
cardinality: 'authority-singleton',
authorityCoordinatorActive: true,
ownerNodeId: 'owner-b',
authorityLease: activeLease,
});
assert.equal(nonOwned.allowed, false);
assert.equal(nonOwned.reason, 'AUTHORITY_COORDINATOR_LEASE_NOT_OWNED');
const fenced = decideMatrixPlacementBind({
authorityRoot: 'SPACE-HIVECAST-LAB',
mount: 'system.registry',
cardinality: 'authority-singleton',
authorityCoordinatorActive: true,
ownerNodeId: 'owner-a',
authorityLease: {
...activeLease,
status: 'fenced',
reason: 'LEASE_FORCED',
},
});
assert.equal(fenced.allowed, false);
assert.equal(fenced.reason, 'AUTHORITY_COORDINATOR_LEASE_NOT_OWNED');
assert.equal(decideMatrixPlacementBind({
authorityRoot: 'SPACE-HIVECAST-LAB',
mount: 'system.registry',
cardinality: 'authority-singleton',
authorityCoordinatorActive: true,
ownerNodeId: 'owner-a',
}).allowed, true);
});
it('claims webapp surfaces in system.registry for shell launchers', async () => {
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-webapp-surface-test',
root: 'SPACE-RUNNER',
}),
logging: false,
});
try {
const registration = await registerRunnerRuntime(
runtime,
runtimePresenceManifest,
undefined,
undefined,
'runtime-presence-test',
undefined,
undefined,
{
appName: 'director',
routePrefix: '/apps/director/',
displayName: 'Director',
icon: 'director',
navOrder: 20,
description: 'Matrix actor hierarchy explorer',
shells: ['platform', 'edge'],
assetMount: 'system.runtimes.runtime-presence-runner.http',
},
);
const appEntries = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ namespaceRoot: 'SPACE-RUNNER', kind: 'app', openable: true },
{ timeoutMs: 5_000 },
) as {
entries?: Array<{
mount?: string;
app?: { appName?: string; route?: string; title?: string; icon?: string; openable?: boolean };
metadata?: { source?: string; assetMount?: string; routeKind?: string; description?: string; navOrder?: number; shells?: string[] };
placement?: { runtimeId?: string; packageRef?: string };
}>;
};
assert.ok(registration.inventoryClaimCount >= 1);
assert.deepEqual(appEntries.entries?.map((entry) => entry.mount), ['director']);
assert.equal(appEntries.entries?.[0]?.app?.appName, 'director');
assert.equal(appEntries.entries?.[0]?.app?.title, 'Director');
assert.equal(appEntries.entries?.[0]?.app?.icon, 'director');
assert.equal(appEntries.entries?.[0]?.app?.route, '/apps/director/');
assert.equal(appEntries.entries?.[0]?.app?.openable, true);
assert.equal(appEntries.entries?.[0]?.metadata?.source, 'runner-webapp-surface');
assert.equal(appEntries.entries?.[0]?.metadata?.description, 'Matrix actor hierarchy explorer');
assert.equal(appEntries.entries?.[0]?.metadata?.navOrder, 20);
assert.deepEqual(appEntries.entries?.[0]?.metadata?.shells, ['platform', 'edge']);
assert.equal(appEntries.entries?.[0]?.metadata?.routeKind, 'matrix-asset-endpoint');
assert.equal(appEntries.entries?.[0]?.metadata?.assetMount, 'system.runtimes.runtime-presence-runner.http');
assert.equal(appEntries.entries?.[0]?.placement?.runtimeId, 'runtime-presence-runner');
assert.equal(appEntries.entries?.[0]?.placement?.packageRef, '@open-matrix/runtime-presence-test');
await runtime.remove('system.registry');
await runtime.createSupervised(ServiceRegistryActor, 'system.registry');
const afterWipe = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ namespaceRoot: 'SPACE-RUNNER', kind: 'app', openable: true },
{ timeoutMs: 5_000 },
) as { entries?: Array<{ mount?: string }> };
assert.deepEqual(afterWipe.entries ?? [], []);
const heartbeat = await heartbeatRunnerRuntime(runtime, registration);
assert.equal(heartbeat?.known, false);
const reclaimed = await registration.reclaim();
assert.ok(
reclaimed.inventoryClaimCount >= 1,
'webapp runtime reclaim re-issues the app-surface registry claim after a SYSTEM restart',
);
const reclaimedAppEntries = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ namespaceRoot: 'SPACE-RUNNER', kind: 'app', openable: true },
{ timeoutMs: 5_000 },
) as { entries?: Array<{ mount?: string; metadata?: { source?: string } }> };
assert.deepEqual(reclaimedAppEntries.entries?.map((entry) => entry.mount), ['director']);
assert.equal(reclaimedAppEntries.entries?.[0]?.metadata?.source, 'runner-webapp-surface');
} finally {
await runtime.shutdown();
}
});
it('projects Host identity into Service Registry placement', async () => {
const home = mkdtempSync(join(tmpdir(), 'runner-host-placement-'));
mkdirSync(join(home, 'credentials'), { recursive: true });
writeFileSync(join(home, 'credentials', 'hivecast-install.json'), JSON.stringify({
version: 1,
installId: 'install_test000000000003',
hostName: 'Test Device',
}));
writeFileSync(join(home, 'credentials', 'hivecast-link.json'), JSON.stringify({
version: 1,
hostId: 'install_test000000000003',
hostName: 'Test Device',
deviceSlug: 'test-device',
}));
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-host-placement-test',
root: 'SPACE-RUNNER',
}),
logging: false,
});
runtime.registerService('matrix-host-home', home);
const observed: Array<{
runtimeId: string;
metadata?: Record<string, unknown>;
}> = [];
const unsubscribe = subscribeRuntimePresence(
runtime.getRootContext(),
'SPACE-RUNNER',
(record) => {
observed.push({
runtimeId: record.runtimeId,
...(record.metadata ? { metadata: record.metadata } : {}),
});
},
);
try {
await registerRunnerRuntime(
runtime,
runtimePresenceManifest,
undefined,
undefined,
'runtime-presence-test',
undefined,
undefined,
{
appName: 'director',
routePrefix: '/apps/director/',
assetMount: 'system.runtimes.runtime-presence-runner.http',
},
);
const appEntries = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ namespaceRoot: 'SPACE-RUNNER', kind: 'app', openable: true },
{ timeoutMs: 5_000 },
) as {
entries?: Array<{
placement?: { deviceId?: string; hostId?: string };
metadata?: { deviceId?: string; hostId?: string; hostName?: string; deviceSlug?: string };
}>;
};
// Slice 1b: real registry app claim still carries Host identity.
assert.equal(appEntries.entries?.[0]?.placement?.deviceId, 'install_test000000000003');
assert.equal(appEntries.entries?.[0]?.placement?.hostId, 'install_test000000000003');
assert.equal(appEntries.entries?.[0]?.metadata?.hostName, 'Test Device');
assert.equal(appEntries.entries?.[0]?.metadata?.deviceSlug, 'test-device');
// Runtime identity in this test is now asserted via presence (the
// runtime directory authority), not via a registry kind=runtime claim.
assert.equal(observed[0]?.metadata?.deviceId, 'install_test000000000003');
assert.equal(observed[0]?.metadata?.hostName, 'Test Device');
} finally {
unsubscribe();
await runtime.shutdown();
rmSync(home, { recursive: true, force: true });
}
});
it('does not require Host placement before a fresh local install has identity files', async () => {
const home = mkdtempSync(join(tmpdir(), 'runner-host-placement-empty-'));
mkdirSync(join(home, 'credentials'), { recursive: true });
const runtime = new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'runner-host-placement-empty-test',
root: 'SPACE-RUNNER',
}),
logging: false,
});
runtime.registerService('matrix-host-home', home);
try {
await registerRunnerRuntime(
runtime,
runtimePresenceManifest,
undefined,
undefined,
'runtime-presence-test',
undefined,
undefined,
{
appName: 'director',
routePrefix: '/apps/director/',
assetMount: 'system.runtimes.runtime-presence-runner.http',
},
);
const appEntries = await RequestReply.execute(
runtime.getRootContext(),
'system.registry',
'registry.query',
{ namespaceRoot: 'SPACE-RUNNER', kind: 'app', openable: true },
{ timeoutMs: 5_000 },
) as {
entries?: Array<{
placement?: { deviceId?: string; hostId?: string };
metadata?: { deviceId?: string; hostId?: string };
}>;
};
assert.equal(appEntries.entries?.[0]?.placement?.deviceId, undefined);
assert.equal(appEntries.entries?.[0]?.placement?.hostId, undefined);
assert.equal(appEntries.entries?.[0]?.metadata?.deviceId, undefined);
assert.equal(appEntries.entries?.[0]?.metadata?.hostId, undefined);
} finally {
await runtime.shutdown();
rmSync(home, { recursive: true, force: true });
}
});
});

View File

@ -0,0 +1,46 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.js';
import { createRunnerRuntimeHandle } from '../../src/utils/runner-runtime.js';
class FactoryProbeActor extends MatrixActor {}
describe('runner runtime security policy', () => {
it('starts production runner runtimes with standard policy and dynamic compilation disabled', async () => {
const handle = await createRunnerRuntimeHandle('COM.TEST.RUNNER-POLICY');
try {
assert.equal(handle.runtime.isDynamicCompilationAllowed(), false);
const rootContext = handle.runtime.getRootContext();
const policyEngine = rootContext.securityRealm?.policyEngine;
assert.ok(policyEngine, 'runner runtime should install a policy engine');
assert.equal(
policyEngine.evaluate({
op: 'factory.create-from-code',
payload: {},
sender: 'test.probe',
meta: { topic: 'factory-probe.$inbox', ts: Date.now() },
}).action,
'deny',
);
const actor = new FactoryProbeActor();
await actor.initialize(rootContext.derive('factory-probe'), 'factory-probe');
await assert.rejects(
() => RequestReply.execute(
rootContext,
'factory-probe',
'factory.create-from-code',
{ name: 'probe', code: 'class Probe extends MatrixActor {}' },
{ timeoutMs: 1_000 },
),
/Dynamic component compilation denied by standard policy/,
);
} finally {
await handle.shutdown();
}
});
});

View File

@ -0,0 +1,97 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
resolveNatsBinary,
resolveRunnerTransportPlan,
} from '../../src/utils/runner-transport.js';
import type { ILoadedMatrixPackageEnvironment } from '../../src/utils/package-environment.js';
function createLoadedEnvironment(
packageDir: string,
envName: string,
environment: ILoadedMatrixPackageEnvironment['environment'],
): ILoadedMatrixPackageEnvironment {
return {
packageDir,
envName,
environmentPath: path.join(packageDir, '.matrix', `${envName}.environment.json`),
environment,
};
}
describe('runner transport resolution', () => {
it('prefers environment-owned embedded NATS paths over shared defaults', () => {
const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-runner-embedded-'));
const loaded = createLoadedEnvironment(packageDir, 'dev', {
name: 'dev',
runtime: { root: 'COM.TEST.EMBEDDED' },
nats: {
mode: 'embedded',
port: 5111,
wsPort: 5112,
binaryPath: './tools/nats-server-custom',
dataDir: './runtime-state/nats-data',
pidFile: './runtime-state/nats.pid',
},
});
const plan = resolveRunnerTransportPlan(packageDir, loaded);
assert.equal(plan.mode, 'embedded');
assert.equal(plan.root, 'COM.TEST.EMBEDDED');
assert.equal(plan.url, 'nats://127.0.0.1:5111');
assert.equal(plan.wsUrl, 'ws://127.0.0.1:5112');
assert.equal(plan.binaryPath, path.join(packageDir, 'tools', 'nats-server-custom'));
assert.equal(plan.dataDir, path.join(packageDir, 'runtime-state', 'nats-data'));
assert.equal(plan.pidFile, path.join(packageDir, 'runtime-state', 'nats.pid'));
});
it('uses MATRIX_NATS_SERVER_BINARY before shared repo fallbacks', () => {
const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-runner-envbinary-'));
const fakeBinary = path.join(packageDir, 'custom-bin', 'nats-server');
fs.mkdirSync(path.dirname(fakeBinary), { recursive: true });
fs.writeFileSync(fakeBinary, '', 'utf8');
const original = process.env.MATRIX_NATS_SERVER_BINARY;
process.env.MATRIX_NATS_SERVER_BINARY = fakeBinary;
try {
const resolved = resolveNatsBinary(packageDir);
assert.equal(resolved, fakeBinary);
} finally {
if (original === undefined) {
delete process.env.MATRIX_NATS_SERVER_BINARY;
} else {
process.env.MATRIX_NATS_SERVER_BINARY = original;
}
}
});
it('resolves external NATS environments without inventing embedded state', () => {
const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-runner-external-'));
const loaded = createLoadedEnvironment(packageDir, 'integration', {
name: 'integration',
runtime: { root: 'COM.TEST.EXTERNAL' },
nats: {
mode: 'external',
url: 'nats://example.net:4229',
wsUrl: 'wss://example.net/nats-ws',
credentialsRef: 'file:~/.matrix/credentials.json',
},
});
const plan = resolveRunnerTransportPlan(packageDir, loaded, 'COM.TEST.EXTERNAL.OVERRIDE');
assert.equal(plan.mode, 'external');
assert.equal(plan.root, 'COM.TEST.EXTERNAL.OVERRIDE');
assert.equal(plan.url, 'nats://example.net:4229');
assert.equal(plan.wsUrl, 'wss://example.net/nats-ws');
assert.equal(plan.credentialsRef, 'file:~/.matrix/credentials.json');
assert.equal('binaryPath' in plan, false);
assert.equal('dataDir' in plan, false);
assert.equal('pidFile' in plan, false);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,244 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import type { ILoadedMatrixPackageEnvironment } from '../../src/utils/package-environment.js';
import type { IResolvedRunnerTransportPlan } from '../../src/utils/runner-transport.js';
import type { ILoadedMatrixWebappManifest } from '../../src/utils/webapp-manifest.js';
import { startStandaloneWebappServer } from '../../src/utils/standalone-webapp-server.js';
async function getAvailablePort(): Promise<number> {
const server = await new Promise<net.Server>((resolve, reject) => {
const candidate = net.createServer();
candidate.once('error', reject);
candidate.listen(0, '127.0.0.1', () => resolve(candidate));
});
const address = server.address();
assert.ok(address && typeof address === 'object');
const port = address.port;
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
return port;
}
describe('standalone webapp server', () => {
it('advertises JWT browser auth when the runner transport uses credentialsRef', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'standalone-webapp-server-'));
const packageDir = path.join(tempRoot, 'package');
const distDir = path.join(packageDir, 'dist');
fs.mkdirSync(distDir, { recursive: true });
const httpPort = await getAvailablePort();
const credentialsPath = path.join(tempRoot, 'credentials.json');
fs.writeFileSync(credentialsPath, JSON.stringify({
jwt: 'sample.jwt.value',
seed: 'SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
}), 'utf8');
fs.writeFileSync(
path.join(distDir, 'index.html'),
'<!doctype html><html><body>fixture</body></html>',
'utf8',
);
const loadedEnvironment: ILoadedMatrixPackageEnvironment = {
packageDir,
envName: 'host',
environmentPath: path.join(packageDir, '.matrix', 'host.environment.json'),
environment: {
name: 'host',
runtime: { root: 'COM.TEST.EXTERNAL' },
nats: {
mode: 'external',
url: 'nats://example.test:4222',
wsUrl: 'wss://example.test/nats-ws',
credentialsRef: `file:${credentialsPath}`,
},
http: {
enabled: true,
port: httpPort,
},
},
};
const webapp: ILoadedMatrixWebappManifest = {
packageDir,
packageName: '@acme/webapp',
manifestPath: path.join(packageDir, 'matrix.json'),
distDir,
entryFile: 'index.html',
appName: 'webapp',
};
const runnerTransport: IResolvedRunnerTransportPlan = {
envName: 'host',
root: 'COM.TEST.EXTERNAL',
mode: 'external',
url: 'nats://example.test:4222',
wsUrl: 'wss://example.test/nats-ws',
credentialsRef: `file:${credentialsPath}`,
};
const server = await startStandaloneWebappServer({
loadedEnvironment,
webapp,
runnerTransport,
});
try {
const bootstrap = await fetch(`${server.baseUrl}/api/bootstrap`);
assert.equal(bootstrap.status, 200);
const bootstrapPayload = await bootstrap.json() as {
authorityContext?: {
assetHostRoot?: string;
platformServiceRoot?: string;
sessionAuthorityRoot?: string | null;
selectedAuthorityRoot?: string | null;
runtimeInstanceRoot?: string | null;
isAuthenticated?: boolean;
isPlatformAdmin?: boolean;
selectableAuthorityRoots?: string[];
};
authorityRoot?: string;
busRoot?: string;
runtimeInstanceRoot?: string | null;
transportPlan?: {
authMode?: string;
authEndpoint?: string | null;
wsPath?: string;
};
sources?: {
transportAuthMode?: string;
};
};
assert.deepEqual(bootstrapPayload.authorityContext, {
assetHostRoot: 'COM.TEST.EXTERNAL',
platformServiceRoot: 'COM.TEST.EXTERNAL',
sessionAuthorityRoot: 'COM.TEST.EXTERNAL',
selectedAuthorityRoot: 'COM.TEST.EXTERNAL',
runtimeInstanceRoot: null,
isAuthenticated: true,
isPlatformAdmin: false,
selectableAuthorityRoots: ['COM.TEST.EXTERNAL'],
});
assert.equal(bootstrapPayload.authorityRoot, 'COM.TEST.EXTERNAL');
assert.equal(bootstrapPayload.busRoot, 'COM.TEST.EXTERNAL');
assert.equal(bootstrapPayload.runtimeInstanceRoot, null);
assert.equal(bootstrapPayload.transportPlan?.wsPath, '/nats-ws');
assert.equal(bootstrapPayload.transportPlan?.authMode, 'jwt');
assert.equal(bootstrapPayload.transportPlan?.authEndpoint, '/api/nats-jwt');
assert.equal(bootstrapPayload.sources?.transportAuthMode, 'credentials-ref');
const auth = await fetch(`${server.baseUrl}/api/nats-jwt`, { method: 'POST' });
assert.equal(auth.status, 200);
const authPayload = await auth.json() as {
mode?: string;
jwt?: string;
seed?: string;
};
assert.equal(authPayload.mode, 'jwt');
assert.equal(authPayload.jwt, 'sample.jwt.value');
assert.equal(authPayload.seed, 'SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
} finally {
await server.shutdown();
}
});
it('forwards explicit upstream WebSocket paths without duplicating /nats-ws', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'standalone-webapp-server-proxy-'));
const packageDir = path.join(tempRoot, 'package');
const distDir = path.join(packageDir, 'dist');
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(
path.join(distDir, 'index.html'),
'<!doctype html><html><body>fixture</body></html>',
'utf8',
);
const httpPort = await getAvailablePort();
const upstreamPort = await getAvailablePort();
let capturedRequest = '';
const upstream = net.createServer();
const captured = new Promise<void>((resolve) => {
upstream.on('connection', (socket) => {
socket.once('data', (chunk: Buffer) => {
capturedRequest = chunk.toString('utf8');
socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
resolve();
});
});
});
await new Promise<void>((resolve, reject) => {
upstream.once('error', reject);
upstream.listen(upstreamPort, '127.0.0.1', () => {
upstream.off('error', reject);
resolve();
});
});
const loadedEnvironment: ILoadedMatrixPackageEnvironment = {
packageDir,
envName: 'host',
environmentPath: path.join(packageDir, '.matrix', 'host.environment.json'),
environment: {
name: 'host',
runtime: { root: 'COM.TEST.EXTERNAL' },
nats: {
mode: 'external',
url: `nats://127.0.0.1:${upstreamPort}`,
wsUrl: `ws://127.0.0.1:${upstreamPort}/nats-ws`,
},
http: {
enabled: true,
port: httpPort,
},
},
};
const webapp: ILoadedMatrixWebappManifest = {
packageDir,
packageName: '@acme/webapp',
manifestPath: path.join(packageDir, 'matrix.json'),
distDir,
entryFile: 'index.html',
appName: 'webapp',
};
const runnerTransport: IResolvedRunnerTransportPlan = {
envName: 'host',
root: 'COM.TEST.EXTERNAL',
mode: 'external',
url: `nats://127.0.0.1:${upstreamPort}`,
wsUrl: `ws://127.0.0.1:${upstreamPort}/nats-ws`,
};
const server = await startStandaloneWebappServer({
loadedEnvironment,
webapp,
runnerTransport,
});
const client = net.connect(httpPort, '127.0.0.1');
try {
await new Promise<void>((resolve) => {
client.once('connect', resolve);
});
client.write([
'GET /nats-ws?probe=1 HTTP/1.1',
`Host: 127.0.0.1:${httpPort}`,
'Connection: Upgrade',
'Upgrade: websocket',
'Origin: http://127.0.0.1:9999',
'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version: 13',
'',
'',
].join('\r\n'));
await captured;
assert.match(capturedRequest, /^GET \/nats-ws\?probe=1 HTTP\/1\.1\r\n/);
assert.doesNotMatch(capturedRequest, /\/nats-ws\/nats-ws/);
assert.doesNotMatch(capturedRequest, /\r\nOrigin:/i);
} finally {
client.destroy();
await server.shutdown();
await new Promise<void>((resolve) => {
upstream.close(() => resolve());
});
}
});
});

View File

@ -0,0 +1,992 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { randomBytes } from 'node:crypto';
type JsonRecord = Record<string, unknown>;
export interface IHiveCastNatsConfig {
readonly url: string;
readonly wsUrl?: string;
readonly leafnodeUrl?: string;
}
export interface IHiveCastNatsCredentials {
readonly accountSeed?: string;
readonly jwt?: string;
readonly seed?: string;
}
export interface IHiveCastHostLinkRecord {
readonly version: 1;
readonly cloudUrl: string;
readonly deviceRegistryRoot?: string;
readonly hostLinkId?: string;
readonly hostId?: string;
readonly principalId?: string;
readonly hostName?: string;
readonly deviceSlug?: string;
readonly authorityCoordinator?: boolean;
readonly spaceId?: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly authorityRoot: string;
readonly linkedAt: string;
readonly credentials: {
readonly hostLinkTokenRef?: string;
readonly registryTokenRef?: string;
readonly natsCredentialRef: string;
readonly natsLeafnodeCredentialRef?: string;
};
}
interface ILocalOwnerTransportSnapshot {
readonly root: string;
readonly authorityRoot?: string;
readonly nats: JsonRecord;
}
export interface IHiveCastLocalOwnerTransport {
readonly root: string;
readonly authorityRoot?: string;
}
interface ILinkedAuthorityTransportSnapshot {
readonly authorityRoot: string;
readonly nats: JsonRecord;
readonly leafnode?: {
readonly url: string;
readonly credentialsRef: string;
};
readonly authorityCoordinator?: boolean;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly spaceId?: string;
}
export interface IHiveCastLinkPaths {
readonly matrixHome: string;
readonly credentialsDir: string;
readonly installIdentityPath: string;
readonly linkPath: string;
readonly natsCredentialsPath: string;
readonly natsLeafnodeCredentialsPath: string;
readonly hostLinkTokenPath: string;
readonly hostCredentialsPath: string;
readonly hostConfigPath: string;
}
export interface IHiveCastInstallIdentity {
readonly version: 1;
readonly installId: string;
readonly hostName?: string;
readonly createdAt: string;
readonly updatedAt: string;
}
export interface ISaveHiveCastHostLinkOptions {
readonly cwd: string;
readonly matrixHome?: string;
readonly cloudUrl: string;
readonly deviceRegistryRoot?: string;
readonly authorityRoot: string;
readonly nats: IHiveCastNatsConfig;
readonly natsCredentials: IHiveCastNatsCredentials;
readonly hostLinkToken?: string;
readonly hostLinkId?: string;
readonly hostId?: string;
readonly principalId?: string;
readonly hostName?: string;
readonly deviceSlug?: string;
readonly authorityCoordinator?: boolean;
readonly spaceId?: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly linkedAt?: string;
}
export interface IHiveCastLinkStatus {
readonly linked: boolean;
readonly matrixHome: string;
readonly linkPath: string;
readonly natsCredentialsPath: string;
readonly hostConfigPath: string;
readonly natsCredentialsPresent: boolean;
readonly hostConfigPresent: boolean;
readonly hostConfigLinked: boolean;
readonly hostLinkId?: string;
readonly authorityRoot?: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly spaceId?: string;
readonly authorityCoordinator?: boolean;
readonly cloudUrl?: string;
readonly linkedAt?: string;
}
export interface IRemoveHiveCastHostLinkResult {
readonly removed: boolean;
readonly hostConfigReset: boolean;
readonly paths: IHiveCastLinkPaths;
}
export interface IRemoveHiveCastHostLinkOptions {
readonly preserveCredentials?: boolean;
}
const DEFAULT_MATRIX_HOME = path.join(os.homedir(), '.matrix');
const HIVECAST_NATS_JSON_CREDENTIAL_REF = 'file:credentials/hivecast-nats.json';
const HIVECAST_NATS_LEAFNODE_CREDENTIAL_REF = 'file:credentials/hivecast-nats.creds';
const HIVECAST_DEFAULT_RUNTIME_KEYS = [
'SYSTEM',
'GATEWAY',
'INFERENCE',
'AGENTS',
'WEB',
'EDGE',
'DIRECTOR',
'CHAT',
'INFERENCE-SETTINGS',
'FLOWPAD',
'SMITHERS',
'HOST-CONTROL',
'HOST-OPS',
] as const;
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function optionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function expandHome(value: string): string {
if (value === '~') {
return os.homedir();
}
return value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value;
}
export function resolveHiveCastMatrixHome(cwd: string, matrixHome?: string): string {
const raw = matrixHome
?? process.env.MATRIX_HOST_HOME
?? process.env.MATRIX_HOME
?? DEFAULT_MATRIX_HOME;
const expanded = expandHome(raw);
return path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(cwd, expanded);
}
export function hiveCastLinkPaths(cwd: string, matrixHome?: string): IHiveCastLinkPaths {
const resolvedHome = resolveHiveCastMatrixHome(cwd, matrixHome);
const credentialsDir = path.join(resolvedHome, 'credentials');
return {
matrixHome: resolvedHome,
credentialsDir,
installIdentityPath: path.join(credentialsDir, 'hivecast-install.json'),
linkPath: path.join(credentialsDir, 'hivecast-link.json'),
natsCredentialsPath: path.join(credentialsDir, 'hivecast-nats.json'),
natsLeafnodeCredentialsPath: path.join(credentialsDir, 'hivecast-nats.creds'),
hostLinkTokenPath: path.join(credentialsDir, 'hivecast-host-link-token'),
hostCredentialsPath: path.join(resolvedHome, 'credentials.json'),
hostConfigPath: path.join(resolvedHome, 'host.json'),
};
}
export function ensureHiveCastInstallIdentity(options: {
readonly cwd: string;
readonly matrixHome?: string;
readonly hostName?: string;
}): { readonly identity: IHiveCastInstallIdentity; readonly paths: IHiveCastLinkPaths } {
const paths = hiveCastLinkPaths(options.cwd, options.matrixHome);
const existing = readJsonFile(paths.installIdentityPath);
const existingInstallId = optionalString(existing?.installId);
const now = new Date().toISOString();
const hostName = optionalString(options.hostName);
if (existingInstallId) {
const existingHostName = optionalString(existing?.hostName);
const nextHostName = hostName ?? existingHostName;
const identity: IHiveCastInstallIdentity = {
version: 1,
installId: existingInstallId,
...(nextHostName ? { hostName: nextHostName } : {}),
createdAt: optionalString(existing?.createdAt) ?? now,
updatedAt: hostName && hostName !== existingHostName ? now : (optionalString(existing?.updatedAt) ?? now),
};
if (hostName && hostName !== existingHostName) {
writePrivateJson(paths.installIdentityPath, identity);
}
return { identity, paths };
}
const identity: IHiveCastInstallIdentity = {
version: 1,
installId: `install_${randomBytes(10).toString('hex')}`,
...(hostName ? { hostName } : {}),
createdAt: now,
updatedAt: now,
};
writePrivateJson(paths.installIdentityPath, identity);
return { identity, paths };
}
function localOwnerRootToken(installId: string): string {
const token = installId
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
if (!token) {
throw new Error('HIVECAST_INSTALL_IDENTITY_INVALID: installId cannot produce a local owner root');
}
return token;
}
function localOwnerAuthorityRoot(matrixHome: string): string {
const { identity } = ensureHiveCastInstallIdentity({ cwd: process.cwd(), matrixHome });
return `local-${localOwnerRootToken(identity.installId)}`;
}
function runtimeScopeToken(installId: string): string {
const token = installId
.toUpperCase()
.replace(/[^A-Z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
if (!token) {
throw new Error('HIVECAST_INSTALL_IDENTITY_INVALID: installId cannot produce a runtime scope');
}
return token;
}
function installRuntimeScope(matrixHome: string): string {
const { identity } = ensureHiveCastInstallIdentity({ cwd: process.cwd(), matrixHome });
return runtimeScopeToken(identity.installId);
}
function writePrivateJson(filePath: string, payload: object): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
try {
fs.chmodSync(filePath, 0o600);
} catch {
}
}
function writePrivateText(filePath: string, payload: string): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, payload, 'utf8');
try {
fs.chmodSync(filePath, 0o600);
} catch {
}
}
function natsUserCredentialsFile(credentials: IHiveCastNatsCredentials): string | undefined {
const jwt = optionalString(credentials.jwt);
const seed = optionalString(credentials.seed);
if (!jwt || !seed) {
return undefined;
}
return [
'-----BEGIN NATS USER JWT-----',
jwt,
'------END NATS USER JWT------',
'',
'-----BEGIN USER NKEY SEED-----',
seed,
'------END USER NKEY SEED------',
'',
].join('\n');
}
function ownerForPath(filePath: string): { readonly uid: number; readonly gid: number } {
const stat = fs.statSync(filePath);
return { uid: stat.uid, gid: stat.gid };
}
function alignOwnership(filePath: string, owner: { readonly uid: number; readonly gid: number }): void {
if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
return;
}
fs.chownSync(filePath, owner.uid, owner.gid);
}
function readJsonFile(filePath: string): JsonRecord | null {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
return asRecord(parsed);
} catch {
return null;
}
}
function buildHostConfig(
existing: JsonRecord | null,
matrixHome: string,
localOwnerTransportOverride?: ILocalOwnerTransportSnapshot,
linkedAuthorityTransport?: ILinkedAuthorityTransportSnapshot,
): JsonRecord {
const currentHttp = asRecord(existing?.http);
const currentRuntimeStorage = asRecord(existing?.runtimeStorage);
const currentPackageStorage = asRecord(existing?.packageStorage);
const currentAuth = asRecord(existing?.auth);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(matrixHome, existing)
?? localOwnerTransportOverride
?? captureLocalOwnerTransport(matrixHome, existing);
const generatedLocalOwnerRoot = localOwnerAuthorityRoot(matrixHome);
const generatedLocalOwnerTransport = {
root: generatedLocalOwnerRoot,
authorityRoot: generatedLocalOwnerRoot,
addressRoot: generatedLocalOwnerRoot,
nats: defaultLocalOwnerNatsTransport(),
};
const localOwnerHostTransport = localOwnerTransport
? localOwnerHostConfigTransport(localOwnerTransport)
: generatedLocalOwnerTransport;
const hostTransport = linkedAuthorityTransport
? linkedAuthorityTransport.authorityCoordinator === false
? linkedLeafHostConfigTransport(linkedAuthorityTransport, localOwnerHostTransport)
: linkedAuthorityHostConfigTransport(linkedAuthorityTransport)
: localOwnerHostTransport;
const nextConfig: JsonRecord = {
...(existing ?? {}),
kind: optionalString(existing?.kind) ?? 'MatrixHostConfig',
version: typeof existing?.version === 'number' ? existing.version : 1,
home: matrixHome,
...(localOwnerTransport ? { hivecastLocalOwnerTransport: localOwnerTransport } : {}),
http: currentHttp ?? {
host: '127.0.0.1',
port: 3100,
},
transport: hostTransport,
auth: currentAuth ?? {
mode: 'local-client',
},
runtimeStorage: currentRuntimeStorage ?? {
recordsDir: 'runtimes',
logsDir: 'logs/runtimes',
},
packageStorage: currentPackageStorage ?? {
globalDir: 'packages/global',
systemDir: 'packages/system',
},
};
if (linkedAuthorityTransport?.authorityCoordinator === false && linkedAuthorityTransport.leafnode) {
nextConfig.natsTopology = linkedLeafNatsTopology(hostTransport.nats as JsonRecord, linkedAuthorityTransport.leafnode);
} else {
delete nextConfig.natsTopology;
}
return nextConfig;
}
function linkedLeafNatsTopology(
localNats: JsonRecord,
leafnode: { readonly url: string; readonly credentialsRef: string },
): JsonRecord {
const clientUrl = optionalString(localNats.url);
const wsUrl = optionalString(localNats.wsUrl);
return {
mode: 'leafnode',
...(clientUrl ? { clientUrl } : {}),
...(wsUrl ? { wsUrl } : {}),
leafnodes: [{
url: leafnode.url,
credentialsRef: leafnode.credentialsRef,
}],
};
}
function assertLinkedHostConfigConverged(
record: IHiveCastHostLinkRecord,
hostConfig: JsonRecord,
): void {
const transport = asRecord(hostConfig.transport);
const root = optionalString(transport?.root);
const authorityRoot = optionalString(transport?.authorityRoot);
const addressRoot = optionalString(transport?.addressRoot);
for (const [field, actual] of [
['transport.root', root],
['transport.authorityRoot', authorityRoot],
['transport.addressRoot', addressRoot],
] as const) {
if (actual !== record.authorityRoot) {
throw new Error(
`HIVECAST_LINK_ROOT_CONVERGENCE_FAILED: expected host config ${field} ${record.authorityRoot}, got ${actual ?? '(missing)'}`,
);
}
}
}
function linkedAuthorityHostConfigTransport(snapshot: ILinkedAuthorityTransportSnapshot): JsonRecord {
return {
root: snapshot.authorityRoot,
authorityRoot: snapshot.authorityRoot,
addressRoot: snapshot.authorityRoot,
...(snapshot.spaceId ? { spaceId: snapshot.spaceId } : {}),
...(snapshot.routeKey ? { routeKey: snapshot.routeKey } : {}),
...(snapshot.publicNamespace ? { publicNamespace: snapshot.publicNamespace } : {}),
nats: { ...snapshot.nats },
};
}
function linkedLeafHostConfigTransport(
snapshot: ILinkedAuthorityTransportSnapshot,
localOwnerTransport: JsonRecord,
): JsonRecord {
return {
root: snapshot.authorityRoot,
authorityRoot: snapshot.authorityRoot,
addressRoot: snapshot.authorityRoot,
...(snapshot.spaceId ? { spaceId: snapshot.spaceId } : {}),
...(snapshot.routeKey ? { routeKey: snapshot.routeKey } : {}),
...(snapshot.publicNamespace ? { publicNamespace: snapshot.publicNamespace } : {}),
nats: localNatsTransportForLinkedLeaf(snapshot, localOwnerTransport),
};
}
function localNatsTransportForLinkedLeaf(
snapshot: ILinkedAuthorityTransportSnapshot,
localOwnerTransport: JsonRecord,
): JsonRecord {
const localNats = asRecord(localOwnerTransport.nats);
return stripNatsCredentialFields(localNats ?? snapshot.nats);
}
function defaultLocalOwnerNatsTransport(): JsonRecord {
return {
mode: 'external',
url: 'nats://127.0.0.1:4222',
wsUrl: 'ws://127.0.0.1:4223',
port: 4222,
wsPort: 4223,
dataDir: 'nats/host-default',
pidFile: 'nats/host-default/nats-server.pid',
};
}
function stripNatsCredentialFields(nats: JsonRecord): JsonRecord {
const {
credentialsRef: _credentialsRef,
credentials: _credentials,
natsCredentialRef: _natsCredentialRef,
...rest
} = nats;
return rest;
}
function linkedAuthorityNatsTransport(
nats: IHiveCastNatsConfig,
natsCredentialsPath: string,
): JsonRecord {
const url = optionalString(nats.url);
if (!url) {
throw new Error('HIVECAST_LINK_AUTHORITY_NATS_MISSING: linked Device setup requires nats.url from the setup exchange');
}
if (/^wss?:\/\//i.test(url)) {
throw new Error(
`HIVECAST_LINK_AUTHORITY_NATS_WS_ONLY: linked Device setup requires a server NATS URL for runtimes, got ${url}`,
);
}
const wsUrl = optionalString(nats.wsUrl);
return {
mode: 'external',
url,
...(wsUrl ? { wsUrl } : {}),
credentialsRef: `file:${natsCredentialsPath}`,
};
}
function localOwnerHostConfigTransport(snapshot: ILocalOwnerTransportSnapshot): JsonRecord {
return {
root: snapshot.root,
authorityRoot: snapshot.authorityRoot ?? snapshot.root,
addressRoot: snapshot.root,
nats: { ...snapshot.nats },
};
}
function captureLocalOwnerTransport(matrixHome: string, existing: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, asRecord(existing?.transport));
}
function captureLocalOwnerTransportFromCurrentNats(matrixHome: string, existing: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
const transport = asRecord(existing?.transport);
const nats = asRecord(transport?.nats);
if (!nats) return undefined;
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, {
root: localOwnerAuthorityRoot(matrixHome),
nats: { ...nats },
});
}
function readLocalOwnerTransportSnapshot(config: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
const snapshot = asRecord(config?.hivecastLocalOwnerTransport);
if (!snapshot) return undefined;
const root = optionalString(snapshot.root);
const nats = asRecord(snapshot.nats);
if (!root || !nats) return undefined;
return readLocalOwnerTransportSnapshotFromTransport({
root,
...(optionalString(snapshot.authorityRoot) ? { authorityRoot: optionalString(snapshot.authorityRoot) } : {}),
nats: { ...nats },
});
}
function readLocalOwnerTransportSnapshotForHome(
matrixHome: string,
config: JsonRecord | null,
): ILocalOwnerTransportSnapshot | undefined {
const snapshot = readLocalOwnerTransportSnapshot(config);
if (snapshot) return snapshot;
const saved = asRecord(config?.hivecastLocalOwnerTransport);
if (!saved) return undefined;
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, saved);
}
function readLocalOwnerTransportSnapshotFromStatus(matrixHome: string): ILocalOwnerTransportSnapshot | undefined {
const status = readJsonFile(path.join(matrixHome, 'host.status.json'));
if (optionalString(status?.status) !== 'running') return undefined;
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, asRecord(status?.transport));
}
function readLocalOwnerTransportSnapshotFromRuntimeRecords(matrixHome: string): ILocalOwnerTransportSnapshot | undefined {
const runtimesDir = path.join(matrixHome, 'runtimes');
if (!fs.existsSync(runtimesDir)) return undefined;
const localScope = localDefaultRuntimeScope(matrixHome);
const preferredRuntimeIds = HIVECAST_DEFAULT_RUNTIME_KEYS.map((key) => `RUNTIME-HOST-${localScope}-${key}`);
for (const runtimeId of preferredRuntimeIds) {
const snapshot = readLocalOwnerTransportSnapshotFromRuntimeRecord(matrixHome, path.join(runtimesDir, runtimeId, 'runtime.json'));
if (snapshot) return snapshot;
}
for (const entry of fs.readdirSync(runtimesDir, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith(`RUNTIME-HOST-${localScope}-`)) continue;
const snapshot = readLocalOwnerTransportSnapshotFromRuntimeRecord(matrixHome, path.join(runtimesDir, entry.name, 'runtime.json'));
if (snapshot) return snapshot;
}
return undefined;
}
function readLocalOwnerTransportSnapshotFromRuntimeRecord(
matrixHome: string,
recordPath: string,
): ILocalOwnerTransportSnapshot | undefined {
const record = readJsonFile(recordPath);
const metadata = asRecord(record?.metadata);
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, asRecord(metadata?.transport));
}
function readLocalOwnerTransportSnapshotFromTransportForHome(
_matrixHome: string,
transport: JsonRecord | null,
): ILocalOwnerTransportSnapshot | undefined {
return readLocalOwnerTransportSnapshotFromTransport(transport);
}
function readLocalOwnerTransportSnapshotFromTransport(transport: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
if (!transport) return undefined;
if (
optionalString(transport.routeKey)
|| optionalString(transport.publicNamespace)
|| optionalString(transport.spaceId)
) {
return undefined;
}
const root = optionalString(transport.root);
if (!root) return undefined;
const nats = asRecord(transport.nats);
if (!nats || optionalString(nats.credentialsRef)) return undefined;
const mode = optionalString(nats.mode);
if (mode !== 'embedded' && mode !== 'external') return undefined;
const natsPort = positivePort(nats.port) ?? loopbackUrlPort(optionalString(nats.url));
const natsWsPort = positivePort(nats.wsPort) ?? loopbackUrlPort(optionalString(nats.wsUrl));
const maxPayloadBytes = positiveInteger(nats.maxPayloadBytes);
if (!natsPort || !natsWsPort) return undefined;
const natsUrl = mode === 'external'
? optionalString(nats.url) ?? `nats://127.0.0.1:${natsPort}`
: undefined;
const natsWsUrl = mode === 'external'
? optionalString(nats.wsUrl) ?? `ws://127.0.0.1:${natsWsPort}`
: undefined;
const authorityRoot = optionalString(transport.authorityRoot);
return {
root,
...(authorityRoot && authorityRoot !== root ? { authorityRoot } : {}),
nats: {
mode,
...(natsUrl ? { url: natsUrl } : {}),
...(natsWsUrl ? { wsUrl: natsWsUrl } : {}),
port: natsPort,
wsPort: natsWsPort,
...(maxPayloadBytes ? { maxPayloadBytes } : {}),
dataDir: optionalString(nats.dataDir) ?? 'nats/host-default',
pidFile: optionalString(nats.pidFile) ?? 'nats/host-default/nats-server.pid',
...(optionalString(nats.binaryPath) ? { binaryPath: optionalString(nats.binaryPath) } : {}),
},
};
}
export function readHiveCastLocalOwnerTransport(
cwd: string,
matrixHome?: string,
): IHiveCastLocalOwnerTransport | null {
const paths = hiveCastLinkPaths(cwd, matrixHome);
const config = readJsonFile(paths.hostConfigPath);
const snapshot = readLocalOwnerTransportSnapshot(config)
?? readLocalOwnerTransportSnapshotForHome(paths.matrixHome, config)
?? captureLocalOwnerTransport(paths.matrixHome, config)
?? readLocalOwnerTransportSnapshotFromStatus(paths.matrixHome)
?? readLocalOwnerTransportSnapshotFromRuntimeRecords(paths.matrixHome);
if (!snapshot) return null;
return {
root: snapshot.root,
...(snapshot.authorityRoot ? { authorityRoot: snapshot.authorityRoot } : {}),
};
}
function positivePort(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
return undefined;
}
return value;
}
function positiveInteger(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
return undefined;
}
return value;
}
function loopbackUrlPort(raw: string | undefined): number | undefined {
if (!raw) return undefined;
try {
const parsed = new URL(raw);
if (!['127.0.0.1', 'localhost', '[::1]', '::1'].includes(parsed.hostname)) {
return undefined;
}
const port = Number.parseInt(parsed.port, 10);
return Number.isFinite(port) && port > 0 ? port : undefined;
} catch {
return undefined;
}
}
function localDefaultRuntimeScope(matrixHome: string): string {
return installRuntimeScope(matrixHome);
}
function activeDefaultRuntimeScope(matrixHome: string): string {
return localDefaultRuntimeScope(matrixHome);
}
function defaultRuntimeIdsForScope(scope: string): Set<string> {
return new Set(HIVECAST_DEFAULT_RUNTIME_KEYS.map((key) => `RUNTIME-HOST-${scope}-${key}`));
}
function isHiveCastDefaultRuntimeId(runtimeId: string): boolean {
return runtimeId.startsWith('RUNTIME-HOST-')
&& HIVECAST_DEFAULT_RUNTIME_KEYS.some((key) => runtimeId.endsWith(`-${key}`));
}
function updateHiveCastDefaultRuntimePolicies(matrixHome: string, activeScope: string): void {
const runtimesDir = path.join(matrixHome, 'runtimes');
if (!fs.existsSync(runtimesDir)) return;
const activeIds = defaultRuntimeIdsForScope(activeScope);
for (const entry of fs.readdirSync(runtimesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const runtimeId = entry.name;
if (!isHiveCastDefaultRuntimeId(runtimeId)) continue;
const recordPath = path.join(runtimesDir, runtimeId, 'runtime.json');
const record = readJsonFile(recordPath);
if (!record) continue;
const recordOwner = ownerForPath(recordPath);
const active = activeIds.has(runtimeId);
const nextRecord = {
...record,
startup: active ? 'auto' : 'manual',
restart: active ? 'always' : 'never',
updatedAt: new Date().toISOString(),
metadata: {
...(asRecord(record.metadata) ?? {}),
hivecastDefaultRuntimePolicy: active ? 'active' : 'inactive',
hivecastDefaultRuntimePolicyUpdatedAt: new Date().toISOString(),
},
};
writePrivateJson(recordPath, nextRecord);
alignOwnership(recordPath, recordOwner);
}
}
export function saveHiveCastHostLink(options: ISaveHiveCastHostLinkOptions): {
readonly record: IHiveCastHostLinkRecord;
readonly paths: IHiveCastLinkPaths;
} {
const paths = hiveCastLinkPaths(options.cwd, options.matrixHome);
fs.mkdirSync(paths.matrixHome, { recursive: true });
const homeOwner = ownerForPath(paths.matrixHome);
fs.mkdirSync(paths.credentialsDir, { recursive: true });
alignOwnership(paths.credentialsDir, homeOwner);
writePrivateJson(paths.natsCredentialsPath, options.natsCredentials);
alignOwnership(paths.natsCredentialsPath, homeOwner);
writePrivateJson(paths.hostCredentialsPath, options.natsCredentials);
alignOwnership(paths.hostCredentialsPath, homeOwner);
const leafnodeUrl = optionalString(options.nats.leafnodeUrl);
const leafnodeCredentialBody = natsUserCredentialsFile(options.natsCredentials);
if (options.authorityCoordinator !== true && leafnodeUrl) {
if (!leafnodeCredentialBody) {
throw new Error('HIVECAST_LINK_LEAFNODE_CREDENTIALS_MISSING: linked Device leaf-node transport requires jwt+seed credentials');
}
writePrivateText(paths.natsLeafnodeCredentialsPath, leafnodeCredentialBody);
alignOwnership(paths.natsLeafnodeCredentialsPath, homeOwner);
} else {
fs.rmSync(paths.natsLeafnodeCredentialsPath, { force: true });
}
if (options.hostLinkToken) {
writePrivateText(paths.hostLinkTokenPath, `${options.hostLinkToken}\n`);
alignOwnership(paths.hostLinkTokenPath, homeOwner);
} else {
fs.rmSync(paths.hostLinkTokenPath, { force: true });
}
const record: IHiveCastHostLinkRecord = {
version: 1,
cloudUrl: options.cloudUrl,
...(options.deviceRegistryRoot ? { deviceRegistryRoot: options.deviceRegistryRoot } : {}),
...(options.hostLinkId ? { hostLinkId: options.hostLinkId } : {}),
...(options.hostId ? { hostId: options.hostId } : {}),
...(options.principalId ? { principalId: options.principalId } : {}),
...(options.hostName ? { hostName: options.hostName } : {}),
...(options.deviceSlug ? { deviceSlug: options.deviceSlug } : {}),
authorityCoordinator: options.authorityCoordinator === true,
...(options.spaceId ? { spaceId: options.spaceId } : {}),
...(options.routeKey ? { routeKey: options.routeKey } : {}),
...(options.publicNamespace ? { publicNamespace: options.publicNamespace } : {}),
authorityRoot: options.authorityRoot,
linkedAt: options.linkedAt ?? new Date().toISOString(),
credentials: {
...(options.hostLinkToken ? { hostLinkTokenRef: 'file:credentials/hivecast-host-link-token' } : {}),
natsCredentialRef: HIVECAST_NATS_JSON_CREDENTIAL_REF,
...(options.authorityCoordinator !== true && leafnodeUrl
? { natsLeafnodeCredentialRef: HIVECAST_NATS_LEAFNODE_CREDENTIAL_REF }
: {}),
},
};
writePrivateJson(paths.linkPath, record);
alignOwnership(paths.linkPath, homeOwner);
const existingHostConfig = readJsonFile(paths.hostConfigPath);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(paths.matrixHome, existingHostConfig)
?? captureLocalOwnerTransportFromCurrentNats(paths.matrixHome, existingHostConfig)
?? readLocalOwnerTransportSnapshotFromStatus(paths.matrixHome)
?? readLocalOwnerTransportSnapshotFromRuntimeRecords(paths.matrixHome)
?? captureLocalOwnerTransport(paths.matrixHome, existingHostConfig);
const linkedAuthorityTransport: ILinkedAuthorityTransportSnapshot = {
authorityRoot: options.authorityRoot,
...(options.routeKey ? { routeKey: options.routeKey } : {}),
...(options.publicNamespace ? { publicNamespace: options.publicNamespace } : {}),
...(options.spaceId ? { spaceId: options.spaceId } : {}),
authorityCoordinator: options.authorityCoordinator === true,
...(options.authorityCoordinator !== true && leafnodeUrl
? { leafnode: { url: leafnodeUrl, credentialsRef: HIVECAST_NATS_LEAFNODE_CREDENTIAL_REF } }
: {}),
nats: linkedAuthorityNatsTransport(options.nats, paths.natsCredentialsPath),
};
const hostConfig = buildHostConfig(
existingHostConfig,
paths.matrixHome,
localOwnerTransport,
linkedAuthorityTransport,
);
assertLinkedHostConfigConverged(record, hostConfig);
writePrivateJson(paths.hostConfigPath, hostConfig);
alignOwnership(paths.hostConfigPath, homeOwner);
updateHiveCastDefaultRuntimePolicies(paths.matrixHome, activeDefaultRuntimeScope(paths.matrixHome));
return { record, paths };
}
export function readHiveCastHostLink(cwd: string, matrixHome?: string): {
readonly record: IHiveCastHostLinkRecord | null;
readonly paths: IHiveCastLinkPaths;
} {
const paths = hiveCastLinkPaths(cwd, matrixHome);
const parsed = readJsonFile(paths.linkPath);
if (!parsed || parsed.version !== 1) {
return { record: null, paths };
}
const authorityRoot = optionalString(parsed.authorityRoot);
const cloudUrl = optionalString(parsed.cloudUrl);
const credentials = asRecord(parsed.credentials);
const natsCredentialRef = optionalString(credentials?.natsCredentialRef);
const linkedAt = optionalString(parsed.linkedAt);
if (!authorityRoot || !cloudUrl || !natsCredentialRef || !linkedAt) {
return { record: null, paths };
}
const hostId = optionalString(parsed.hostId);
const deviceRegistryRoot = optionalString(parsed.deviceRegistryRoot);
const hostLinkId = optionalString(parsed.hostLinkId);
const principalId = optionalString(parsed.principalId);
const hostName = optionalString(parsed.hostName);
const deviceSlug = optionalString(parsed.deviceSlug);
const authorityCoordinator = typeof parsed.authorityCoordinator === 'boolean'
? parsed.authorityCoordinator
: undefined;
const spaceId = optionalString(parsed.spaceId);
const routeKey = optionalString(parsed.routeKey);
const publicNamespace = optionalString(parsed.publicNamespace);
const hostLinkTokenRef = optionalString(credentials?.hostLinkTokenRef);
const registryTokenRef = optionalString(credentials?.registryTokenRef);
const natsLeafnodeCredentialRef = optionalString(credentials?.natsLeafnodeCredentialRef);
return {
paths,
record: {
version: 1,
cloudUrl,
...(deviceRegistryRoot ? { deviceRegistryRoot } : {}),
...(hostLinkId ? { hostLinkId } : {}),
...(hostId ? { hostId } : {}),
...(principalId ? { principalId } : {}),
...(hostName ? { hostName } : {}),
...(deviceSlug ? { deviceSlug } : {}),
...(typeof authorityCoordinator === 'boolean' ? { authorityCoordinator } : {}),
...(spaceId ? { spaceId } : {}),
...(routeKey ? { routeKey } : {}),
...(publicNamespace ? { publicNamespace } : {}),
authorityRoot,
linkedAt,
credentials: {
natsCredentialRef,
...(hostLinkTokenRef ? { hostLinkTokenRef } : {}),
...(registryTokenRef ? { registryTokenRef } : {}),
...(natsLeafnodeCredentialRef ? { natsLeafnodeCredentialRef } : {}),
},
},
};
}
export function removeHiveCastCredentialFiles(
paths: Pick<IHiveCastLinkPaths, 'natsCredentialsPath' | 'natsLeafnodeCredentialsPath' | 'hostCredentialsPath' | 'hostLinkTokenPath'>,
): boolean {
const existed = fs.existsSync(paths.natsCredentialsPath)
|| fs.existsSync(paths.natsLeafnodeCredentialsPath)
|| fs.existsSync(paths.hostCredentialsPath)
|| fs.existsSync(paths.hostLinkTokenPath);
fs.rmSync(paths.natsCredentialsPath, { force: true });
fs.rmSync(paths.natsLeafnodeCredentialsPath, { force: true });
fs.rmSync(paths.hostCredentialsPath, { force: true });
fs.rmSync(paths.hostLinkTokenPath, { force: true });
return existed;
}
export function removeHiveCastHostLink(
cwd: string,
matrixHome?: string,
options: IRemoveHiveCastHostLinkOptions = {},
): IRemoveHiveCastHostLinkResult {
const paths = hiveCastLinkPaths(cwd, matrixHome);
const existed = fs.existsSync(paths.linkPath)
|| fs.existsSync(paths.natsCredentialsPath)
|| fs.existsSync(paths.natsLeafnodeCredentialsPath)
|| fs.existsSync(paths.hostLinkTokenPath)
|| fs.existsSync(paths.hostCredentialsPath)
|| isHostConfigLinked(paths.hostConfigPath);
fs.rmSync(paths.linkPath, { force: true });
updateHiveCastDefaultRuntimePolicies(paths.matrixHome, localDefaultRuntimeScope(paths.matrixHome));
const hostConfigReset = resetHostConfigToLocalOwner(paths.hostConfigPath);
if (options.preserveCredentials !== true) {
removeHiveCastCredentialFiles(paths);
}
return { removed: existed, hostConfigReset, paths };
}
export function readHiveCastLinkStatus(cwd: string, matrixHome?: string): IHiveCastLinkStatus {
const { record, paths } = readHiveCastHostLink(cwd, matrixHome);
const hostConfigLinked = Boolean(record) && fs.existsSync(paths.hostConfigPath);
return {
linked: Boolean(record),
matrixHome: paths.matrixHome,
linkPath: paths.linkPath,
natsCredentialsPath: paths.natsCredentialsPath,
hostConfigPath: paths.hostConfigPath,
natsCredentialsPresent: fs.existsSync(paths.natsCredentialsPath),
hostConfigPresent: fs.existsSync(paths.hostConfigPath),
hostConfigLinked,
...(record?.hostLinkId ? { hostLinkId: record.hostLinkId } : {}),
...(record?.authorityRoot ? { authorityRoot: record.authorityRoot } : {}),
...(record?.routeKey ? { routeKey: record.routeKey } : {}),
...(record?.publicNamespace ? { publicNamespace: record.publicNamespace } : {}),
...(record?.spaceId ? { spaceId: record.spaceId } : {}),
...(typeof record?.authorityCoordinator === 'boolean' ? { authorityCoordinator: record.authorityCoordinator } : {}),
...(record?.cloudUrl ? { cloudUrl: record.cloudUrl } : {}),
...(record?.linkedAt ? { linkedAt: record.linkedAt } : {}),
};
}
function isHostConfigLinked(hostConfigPath: string): boolean {
const config = readJsonFile(hostConfigPath);
const transport = asRecord(config?.transport);
if (!transport) return false;
const nats = asRecord(transport.nats);
const root = optionalString(transport.root);
const authorityRoot = optionalString(transport.authorityRoot);
const matrixHome = optionalString(config?.home) ?? path.dirname(hostConfigPath);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(matrixHome, config);
const localOwnerRoot = localOwnerTransport?.root;
const hasCloudAuthority = Boolean(
authorityRoot
&& authorityRoot !== root,
) && (localOwnerRoot ? authorityRoot !== localOwnerRoot : true);
const externalNatsIsHiveCast = nats?.mode === 'external'
&& (
Boolean(optionalString(nats.credentialsRef))
|| (!loopbackUrlPort(optionalString(nats.url)) && !loopbackUrlPort(optionalString(nats.wsUrl)))
);
return Boolean(
optionalString(transport.routeKey)
|| optionalString(transport.publicNamespace)
|| optionalString(transport.spaceId)
|| hasCloudAuthority
|| externalNatsIsHiveCast,
);
}
function resetHostConfigToLocalOwner(hostConfigPath: string): boolean {
const config = readJsonFile(hostConfigPath);
if (!config) return false;
const transport = asRecord(config.transport);
if (!transport) return false;
const currentNats = asRecord(transport.nats);
const matrixHome = optionalString(config.home) ?? path.dirname(hostConfigPath);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(matrixHome, config)
?? readLocalOwnerTransportSnapshotFromRuntimeRecords(matrixHome)
?? readLocalOwnerTransportSnapshotFromStatus(matrixHome);
const nextTransport: JsonRecord = { ...transport };
const localRoot = localOwnerAuthorityRoot(matrixHome);
nextTransport.root = localOwnerTransport?.root ?? localRoot;
if (localOwnerTransport?.authorityRoot) {
nextTransport.authorityRoot = localOwnerTransport.authorityRoot;
} else {
delete nextTransport.authorityRoot;
}
nextTransport.addressRoot = localOwnerTransport?.root ?? localRoot;
delete nextTransport.routeKey;
delete nextTransport.publicNamespace;
delete nextTransport.spaceId;
nextTransport.nats = localOwnerTransport?.nats
?? (currentNats && currentNats.mode !== 'external' ? currentNats : defaultLocalOwnerNatsTransport());
const nextConfig: JsonRecord = {
...config,
transport: nextTransport,
};
delete nextConfig.natsTopology;
writePrivateJson(hostConfigPath, nextConfig);
return true;
}

View File

@ -0,0 +1,139 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
type JsonRecord = Record<string, unknown>;
export interface IMatrixPackageEnvironment {
readonly name: string;
readonly nats?: {
readonly mode?: string;
readonly port?: number;
readonly wsPort?: number;
readonly url?: string;
readonly wsUrl?: string;
readonly credentialsRef?: string;
readonly dataDir?: string;
readonly pidFile?: string;
readonly binaryPath?: string;
};
readonly runtime: {
readonly root: string;
readonly runtimeId?: string;
readonly runtimeMount?: string;
readonly controlMount?: string;
};
readonly package?: {
readonly publicRoot?: string;
};
readonly http?: {
readonly enabled?: boolean;
readonly port?: number;
};
readonly host?: {
readonly matrixDir?: string;
};
readonly topology?: Record<string, unknown>;
readonly authorityLease?: Record<string, unknown> | null;
}
export interface ILoadedMatrixPackageEnvironment {
readonly packageDir: string;
readonly envName: string;
readonly environmentPath: string;
readonly environment: IMatrixPackageEnvironment;
}
export function loadPackageEnvironment(packageDir: string, envName: string): ILoadedMatrixPackageEnvironment {
const resolvedPackageDir = path.resolve(packageDir);
const cleanEnvName = envName.trim();
if (cleanEnvName.length === 0) {
throw new Error('Package environment name must be a non-empty string');
}
const environmentPath = path.join(resolvedPackageDir, '.matrix', `${cleanEnvName}.environment.json`);
if (!fs.existsSync(environmentPath)) {
throw new Error(`Package environment "${cleanEnvName}" not found at ${environmentPath}`);
}
return loadPackageEnvironmentFromPath(resolvedPackageDir, environmentPath, cleanEnvName);
}
export function loadPackageEnvironmentFromPath(
packageDir: string,
environmentPath: string,
envName?: string,
): ILoadedMatrixPackageEnvironment {
const resolvedPackageDir = path.resolve(packageDir);
const resolvedEnvironmentPath = path.resolve(environmentPath);
if (!fs.existsSync(resolvedEnvironmentPath)) {
throw new Error(`Package environment file not found at ${resolvedEnvironmentPath}`);
}
const label = path.basename(resolvedEnvironmentPath);
const parsed = readJsonObject(resolvedEnvironmentPath, label);
const name = readRequiredString(parsed.name, `Package environment "${envName ?? label}" requires name`);
const cleanEnvName = readOptionalString(envName) ?? name;
const runtime = asRecord(parsed.runtime);
const root = readRequiredString(runtime?.root, `Package environment "${cleanEnvName}" requires runtime.root`);
const environment: IMatrixPackageEnvironment = {
name,
runtime: {
root,
...(readOptionalString(runtime?.runtimeId) ? { runtimeId: readOptionalString(runtime?.runtimeId) } : {}),
...(readOptionalString(runtime?.runtimeMount) ? { runtimeMount: readOptionalString(runtime?.runtimeMount) } : {}),
...(readOptionalString(runtime?.controlMount) ? { controlMount: readOptionalString(runtime?.controlMount) } : {}),
},
...(asRecord(parsed.nats) ? { nats: asRecord(parsed.nats) as IMatrixPackageEnvironment['nats'] } : {}),
...(asRecord(parsed.package) ? { package: asRecord(parsed.package) as IMatrixPackageEnvironment['package'] } : {}),
...(asRecord(parsed.http) ? { http: asRecord(parsed.http) as IMatrixPackageEnvironment['http'] } : {}),
...(asRecord(parsed.host) ? { host: asRecord(parsed.host) as IMatrixPackageEnvironment['host'] } : {}),
...(asRecord(parsed.topology) ? { topology: asRecord(parsed.topology) } : {}),
...(parsed.authorityLease === null || asRecord(parsed.authorityLease)
? { authorityLease: parsed.authorityLease === null ? null : asRecord(parsed.authorityLease) }
: {}),
};
return {
packageDir: resolvedPackageDir,
envName: cleanEnvName,
environmentPath: resolvedEnvironmentPath,
environment,
};
}
function readJsonObject(filePath: string, label: string): JsonRecord {
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
if (!isRecord(parsed)) {
throw new Error(`${label} must contain a JSON object`);
}
return parsed;
} catch (err) {
throw new Error(`Failed to parse ${label}: ${err instanceof Error ? err.message : String(err)}`);
}
}
function readRequiredString(value: unknown, message: string): string {
const resolved = readOptionalString(value);
if (!resolved) {
throw new Error(message);
}
return resolved;
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function asRecord(value: unknown): JsonRecord | undefined {
return isRecord(value) ? value : undefined;
}

View File

@ -0,0 +1,150 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { ILoadedMatrixPackageEnvironment } from './package-environment.js';
export interface IResolvedRunnerTransportPlan {
readonly envName: string;
readonly root: string;
readonly mode: 'embedded' | 'external';
readonly url: string;
readonly wsUrl?: string;
readonly credentialsRef?: string;
readonly port?: number;
readonly wsPort?: number;
readonly binaryPath?: string;
readonly dataDir?: string;
readonly pidFile?: string;
}
export function resolveNatsBinary(baseDir: string, explicitBinaryPath?: string): string {
const binaryOverride = readOptionalString(explicitBinaryPath);
if (binaryOverride) {
return path.resolve(baseDir, binaryOverride);
}
const envBinary = readOptionalString(process.env.MATRIX_NATS_SERVER_BINARY)
?? readOptionalString(process.env.NATS_SERVER_BINARY);
if (envBinary) {
return path.resolve(envBinary);
}
const binaryName = process.platform === 'win32' ? 'nats-server.exe' : 'nats-server';
const searchRoots = new Set<string>([
baseDir,
path.resolve(baseDir, '..'),
path.resolve(baseDir, '..', '..'),
path.resolve(baseDir, '..', '..', '..'),
path.resolve(baseDir, '..', '..', '..', '..'),
]);
for (const root of searchRoots) {
const candidates = [
path.resolve(root, 'projects', 'matrix-3', 'packages', 'daemon', 'dist', 'bin', binaryName),
path.resolve(root, 'projects', 'nats-server', binaryName),
path.resolve(root, 'packages', 'daemon', 'dist', 'bin', binaryName),
path.resolve(root, 'nats-server', binaryName),
];
for (const candidate of candidates) {
if (pathExists(candidate)) {
return candidate;
}
}
}
const pathEntries = (process.env.PATH ?? '')
.split(path.delimiter)
.filter((entry) => entry.length > 0);
for (const entry of pathEntries) {
const candidate = path.join(entry, binaryName);
if (pathExists(candidate)) {
return candidate;
}
}
return binaryName;
}
export function resolveRunnerTransportPlan(
packageDir: string,
selectedEnvironment: ILoadedMatrixPackageEnvironment,
rootOverride?: string,
): IResolvedRunnerTransportPlan {
const nats = selectedEnvironment.environment.nats ?? {};
const root = readOptionalString(rootOverride) ?? selectedEnvironment.environment.runtime.root;
const mode = inferMode(nats.mode, nats.url);
if (mode === 'external') {
const url = readOptionalString(nats.url);
if (!url) {
throw new Error(
`Package environment "${selectedEnvironment.envName}" requires nats.url when nats.mode is external`,
);
}
const wsUrl = readOptionalString(nats.wsUrl);
const credentialsRef = readOptionalString(nats.credentialsRef);
return {
envName: selectedEnvironment.envName,
root,
mode: 'external',
url,
...(wsUrl ? { wsUrl } : {}),
...(credentialsRef ? { credentialsRef } : {}),
};
}
const port = readOptionalNumber(nats.port) ?? 4222;
const wsPort = readOptionalNumber(nats.wsPort);
const url = readOptionalString(nats.url) ?? `nats://127.0.0.1:${port}`;
const wsUrl = readOptionalString(nats.wsUrl) ?? (wsPort ? `ws://127.0.0.1:${wsPort}` : undefined);
const dataDir = path.resolve(
packageDir,
readOptionalString(nats.dataDir) ?? path.join('.matrix', 'state', selectedEnvironment.envName, 'nats'),
);
const pidFile = path.resolve(
packageDir,
readOptionalString(nats.pidFile) ?? path.join('.matrix', 'state', selectedEnvironment.envName, 'nats-server.pid'),
);
const binaryPath = resolveNatsBinary(packageDir, readOptionalString(nats.binaryPath));
return {
envName: selectedEnvironment.envName,
root,
mode: 'embedded',
url,
port,
...(wsUrl ? { wsUrl } : {}),
...(wsPort !== undefined ? { wsPort } : {}),
binaryPath,
dataDir,
pidFile,
};
}
function inferMode(mode: string | undefined, url: string | undefined): 'embedded' | 'external' {
const normalizedMode = readOptionalString(mode)?.toLowerCase();
if (normalizedMode === 'external' || readOptionalString(url)) {
return 'external';
}
return 'embedded';
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function readOptionalNumber(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return undefined;
}
return value;
}
function pathExists(filePath: string): boolean {
return fs.existsSync(filePath);
}

View File

@ -0,0 +1,112 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
type JsonRecord = Record<string, unknown>;
export interface ILoadedMatrixWebappManifest {
readonly packageDir: string;
readonly packageName: string;
readonly manifestPath: string;
readonly distDir: string;
readonly entryFile: string;
readonly appName: string;
readonly displayName?: string;
readonly icon?: string;
readonly navOrder?: number;
readonly description?: string;
readonly shells?: readonly string[];
}
export function loadMatrixWebappManifest(packageDir: string): ILoadedMatrixWebappManifest | null {
const resolvedPackageDir = path.resolve(packageDir);
const manifestPath = path.join(resolvedPackageDir, 'matrix.json');
if (!fs.existsSync(manifestPath)) {
return null;
}
const manifest = readJsonObject(manifestPath, 'matrix.json');
const webapp = asRecord(manifest.webapp);
if (!webapp) {
return null;
}
const packageName = readRequiredString(
manifest.name,
`matrix.json in ${resolvedPackageDir} requires a non-empty "name"`,
);
const distDir = path.resolve(
resolvedPackageDir,
readOptionalString(webapp.distDir) ?? 'dist',
);
const entryFile = readOptionalString(webapp.entry) ?? 'index.html';
const appName = readOptionalString(webapp.appName) ?? packageName.replace(/^@[^/]+\//, '');
const displayName = readOptionalString(webapp.displayName);
const icon = readOptionalString(webapp.icon);
const navOrder = readOptionalNumber(webapp.navOrder);
const description = readOptionalString(webapp.description);
const shells = readStringArray(webapp.shells);
return {
packageDir: resolvedPackageDir,
packageName,
manifestPath,
distDir,
entryFile,
appName,
...(displayName ? { displayName } : {}),
...(icon ? { icon } : {}),
...(typeof navOrder === 'number' ? { navOrder } : {}),
...(description ? { description } : {}),
...(shells.length > 0 ? { shells } : {}),
};
}
function readJsonObject(filePath: string, label: string): JsonRecord {
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
if (!isRecord(parsed)) {
throw new Error(`${label} must contain a JSON object`);
}
return parsed;
} catch (error) {
throw new Error(`Failed to parse ${label}: ${error instanceof Error ? error.message : String(error)}`);
}
}
function readRequiredString(value: unknown, message: string): string {
const resolved = readOptionalString(value);
if (!resolved) {
throw new Error(message);
}
return resolved;
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function readOptionalNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function readStringArray(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
const entries = value
.map((entry) => readOptionalString(entry))
.filter((entry): entry is string => typeof entry === 'string');
return [...new Set(entries)];
}
function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function asRecord(value: unknown): JsonRecord | undefined {
return isRecord(value) ? value : undefined;
}

View File

@ -13,8 +13,9 @@ What it proves:
- the SDK packages build from this repo, - the SDK packages build from this repo,
- this package builds against `@open-matrix/core`, - this package builds against `@open-matrix/core`,
- `matrix run` starts the actor from the package manifest, - a local unauthenticated `nats-server` is started for the demo only,
- `matrix invoke demo.greeter echo ...` calls the actor over the SDK local - `matrix package run` starts the actor from the package manifest against that
runtime path, broker,
- all runtime state is written under the demo package's local `.matrix/` - `--check-op echo` calls the actor over the Matrix runtime/NATS transport path,
directory. - no managed host service, machine link, provider account, provider API key, or
local HTTP control server is involved.

View File

@ -1,12 +1,12 @@
import { spawn, spawnSync } from 'node:child_process'; import { spawn, spawnSync } from 'node:child_process';
import { createServer } from 'node:http'; import { createServer, createConnection } from 'node:net';
import { dirname, resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..', '..'); const repoRoot = resolve(__dirname, '..', '..');
const demoDir = __dirname; const demoDir = __dirname;
const matrixCli = resolve(repoRoot, 'packages', 'cli', 'dist', 'bin', 'matrix.js'); const matrixCli = resolve(repoRoot, 'packages', 'cli', 'dist', 'index.js');
function run(command, args, options = {}) { function run(command, args, options = {}) {
const result = spawnSync(command, args, { const result = spawnSync(command, args, {
@ -38,21 +38,27 @@ async function freePort() {
}); });
} }
async function waitForHealth(port, child) { async function waitForTcp(port, child) {
const deadline = Date.now() + 10_000; const deadline = Date.now() + 10_000;
let lastError; let lastError;
while (Date.now() < deadline) { while (Date.now() < deadline) {
if (child.exitCode !== null) throw new Error(`matrix run exited early with ${child.exitCode}`); if (child.exitCode !== null) throw new Error(`nats-server exited early with ${child.exitCode}`);
try { try {
const response = await fetch(`http://127.0.0.1:${port}/healthz`); await new Promise((resolveConnect, rejectConnect) => {
if (response.ok) return; const socket = createConnection({ host: '127.0.0.1', port });
lastError = new Error(`healthz returned ${response.status}`); socket.once('connect', () => {
socket.end();
resolveConnect();
});
socket.once('error', rejectConnect);
});
return;
} catch (error) { } catch (error) {
lastError = error; lastError = error;
} }
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
} }
throw lastError ?? new Error('Timed out waiting for matrix run healthz'); throw lastError ?? new Error('Timed out waiting for nats-server');
} }
function stop(child) { function stop(child) {
@ -70,44 +76,50 @@ function stop(child) {
} }
const port = await freePort(); const port = await freePort();
let runner; let natsServer;
try { try {
run('pnpm', ['run', 'build']); run('pnpm', ['run', 'build']);
run('pnpm', ['--filter', 'matrix-sdk-example-standalone-greeter', 'run', 'build']); run('pnpm', ['--filter', 'matrix-sdk-example-standalone-greeter', 'run', 'build']);
runner = spawn(process.execPath, [ natsServer = spawn('nats-server', ['-p', String(port), '-a', '127.0.0.1'], {
matrixCli, 'run', demoDir,
'--dir', demoDir,
'--port', String(port),
], {
cwd: repoRoot, cwd: repoRoot,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
env: process.env, env: process.env,
}); });
runner.stderr.on('data', (chunk) => process.stderr.write(chunk)); natsServer.stderr.on('data', (chunk) => process.stderr.write(chunk));
await waitForHealth(port, runner); await waitForTcp(port, natsServer);
const invoked = run(process.execPath, [ const invoked = run(process.execPath, [
matrixCli, 'invoke', matrixCli,
'demo.greeter', 'package',
'run',
demoDir,
'--nats-url',
`nats://127.0.0.1:${port}`,
'--root',
'demo',
'--check-op',
'echo', 'echo',
'--check-payload',
'{"message":"hello-from-standalone-demo"}', '{"message":"hello-from-standalone-demo"}',
'--dir', demoDir, '--json',
'--port', String(port),
], { cwd: repoRoot, stdio: 'pipe' }); ], { cwd: repoRoot, stdio: 'pipe' });
const body = JSON.parse(invoked.stdout.trim()); const body = JSON.parse(invoked.stdout.trim());
if (body?.ok !== true || body?.result?.message !== 'hello-from-standalone-demo') { if (body?.ok !== true || body?.check?.result?.message !== 'hello-from-standalone-demo') {
throw new Error(`Unexpected invoke result: ${invoked.stdout}`); throw new Error(`Unexpected package run result: ${invoked.stdout}`);
} }
console.log(JSON.stringify({ console.log(JSON.stringify({
ok: true, ok: true,
demo: 'standalone-greeter', demo: 'standalone-greeter',
mount: 'demo.greeter', mode: body.mode,
port, mount: body.mount,
invoke: body, root: body.root,
natsUrl: body.natsUrl,
authMode: body.authMode,
check: body.check,
}, null, 2)); }, null, 2));
} finally { } finally {
if (runner) await stop(runner); if (natsServer) await stop(natsServer);
} }

View File

@ -11,9 +11,8 @@
"type-check": "pnpm -r --sort --if-present run type-check", "type-check": "pnpm -r --sort --if-present run type-check",
"pack:all": "pnpm --filter './packages/*' -r exec pnpm pack", "pack:all": "pnpm --filter './packages/*' -r exec pnpm pack",
"demo:standalone": "node examples/standalone-greeter/run-demo.mjs", "demo:standalone": "node examples/standalone-greeter/run-demo.mjs",
"demo:hivecast-login": "node examples/hivecast-login/run-demo.mjs", "prove:cli": "pnpm --filter @open-matrix/cli build && pnpm --filter @open-matrix/cli test && pnpm run demo:standalone",
"prove:external-consumer": "node scripts/prove-external-consumer.mjs", "prove:external-consumer": "node scripts/prove-external-consumer.mjs",
"prove:cli-uat": "node scripts/prove-sdk-cli-uat.mjs",
"prove": "pnpm run clean && pnpm run build && pnpm run test && pnpm run type-check && pnpm run pack:all" "prove": "pnpm run clean && pnpm run build && pnpm run test && pnpm run type-check && pnpm run pack:all"
}, },
"dependencies": { "dependencies": {

View File

@ -1,23 +1,43 @@
# @open-matrix/cli # @open-matrix/cli
Package-author CLI for the Matrix SDK layer. Package-author CLI for Matrix SDK.
This package owns the SDK/package-author `matrix` binary. It does not depend on ## Scope
provider overlay packages, managed host packages, platform system actors,
device-link implementations, or broker-authority/signing packages.
Initial command surface: This package owns the `matrix` binary for SDK users who build, package, and run
Matrix actors and browser surfaces.
It does not own managed provider login, machine linking, local platform install,
or managed host supervision. Provider overlays can add those flows by writing
the broker config shape consumed by the SDK runner.
## Common Commands
```bash ```bash
matrix init [dir] matrix init
matrix add actor <name> matrix actor greeter
matrix actor <name> matrix package run . --nats-url nats://127.0.0.1:4222 --root demo --check-op echo
matrix configure --key <key> --cloud <url> --space <space> matrix actor run ./dist/GreeterActor.js --mount demo.greeter --nats-url nats://127.0.0.1:4222 --root demo --check-op echo
matrix run <entry> --mount <mount> matrix config set registry https://registry.npmjs.org/
matrix invoke <mount> <op> [json] matrix registry doctor
``` ```
`matrix run` starts a standalone actor runner in the current folder. Direct runner credentials are resolved from explicit flags, environment
`matrix invoke` sends a Matrix request envelope to that runner through its local variables, package-local `.matrix`, and `~/.matrix`:
control port. Managed host promotion belongs to provider overlays that consume
the SDK; it is not an SDK-layer requirement. ```text
--nats-url / MATRIX_NATS_URL / NATS_URL
--root / --space / MATRIX_ROOT / MATRIX_SPACE
--jwt / MATRIX_NATS_JWT / NATS_JWT
--seed / MATRIX_NATS_SEED / NATS_SEED
./.matrix/credentials/matrix-broker.json
./.matrix/credentials/matrix-nats.json
./.matrix/config.json
~/.matrix/credentials/matrix-broker.json
~/.matrix/credentials/matrix-nats.json
~/.matrix/config.json
```
JWT and seed must be supplied together. If neither is supplied, the runner
connects to the broker without auth, which is intended for local/demo brokers
only.

View File

@ -1,40 +1,36 @@
{ {
"name": "@open-matrix/cli", "name": "@open-matrix/cli",
"version": "0.1.0", "version": "0.1.0",
"description": "Package-author Matrix CLI for SDK projects.", "description": "Matrix package-author CLI.",
"type": "module", "type": "module",
"private": true,
"bin": { "bin": {
"matrix": "./dist/bin/matrix.js" "matrix": "./dist/index.js"
},
"exports": {
".": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.js",
"default": "./dist/cli.js"
},
"./package.json": "./package.json"
}, },
"files": [ "files": [
"dist", "dist/",
"package.json", "README.md"
"README.md",
"CONTRACT.md"
], ],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --external:node:* --external:commander --external:nats --log-level=warning",
"test": "node --test dist/__tests__/*.test.js", "type-check": "tsc -p tsconfig.json --noEmit",
"type-check": "tsc -p tsconfig.json --noEmit" "test": "node -e \"const{globSync}=require('glob');const{execFileSync}=require('child_process');const files=globSync('tests/unit/*.spec.ts');if(!files.length){console.error('No test files found');process.exit(1)}execFileSync(process.execPath,['--import','tsx','--test',...files],{stdio:'inherit'})\""
}, },
"dependencies": { "dependencies": {
"@open-matrix/core": "workspace:*" "@open-matrix/contracts": "workspace:*",
"@open-matrix/core": "workspace:*",
"@open-matrix/federation": "workspace:*",
"commander": "^11.0.0",
"nats": "^2.29.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.0.10", "@types/node": "^25.0.10",
"esbuild": "^0.27.3",
"glob": "^13.0.6",
"tsx": "^4.15.6",
"typescript": "^5.7.0" "typescript": "^5.7.0"
}, },
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20" "node": ">=22"
} }
} }

View File

@ -0,0 +1,125 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { toKebabCase, toPascalCase, validateMatrixManifest, writeJsonFile, generatePackageJson, generateTsconfig } from '../utils/scaffold.js';
export interface IActorElementCommandOptions {
readonly cwd?: string;
readonly outputDir?: string;
}
export interface IActorElementCommandResult {
readonly rootDir: string;
readonly actorClassName: string;
readonly elementClassName: string;
}
function actorTemplate(className: string, matrixActorImport: string): string {
return [
`import { MatrixActor } from ${JSON.stringify(matrixActorImport)};`,
'',
`export class ${className} extends MatrixActor {`,
' static override accepts = {',
" getValue: {},",
' };',
'',
' protected onGetValue(): { value: number } {',
' return { value: 42 };',
' }',
'}',
'',
].join('\n');
}
function elementTemplate(className: string, matrixActorImport: string): string {
return [
`import { MatrixActor } from ${JSON.stringify(matrixActorImport)};`,
'',
`export class ${className} extends MatrixActor {`,
' static override accepts = {',
" render: {},",
' };',
'',
' protected onRender(): { ok: boolean } {',
' return { ok: true };',
' }',
'}',
'',
].join('\n');
}
export async function actorElementCommand(
name: string,
options: IActorElementCommandOptions = {}
): Promise<IActorElementCommandResult> {
const normalizedName = toKebabCase(name);
if (!normalizedName) {
throw new Error('actor-element name must not be empty');
}
const actorClassName = `${toPascalCase(normalizedName)}Actor`;
const elementClassName = `${toPascalCase(normalizedName)}Element`;
const cwd = path.resolve(options.cwd ?? process.cwd());
const rootDir = path.resolve(options.outputDir ?? path.join(cwd, normalizedName));
const matrixActorImport = '@open-matrix/core';
fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'examples'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'skills'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'tests'), { recursive: true });
const manifest = {
name: normalizedName,
version: '0.1.0',
description: `${actorClassName} + ${elementClassName} generated by mx actor-element`,
runtime: { language: 'typescript', entry: 'dist/index.js' },
components: [
{
type: actorClassName,
export: actorClassName,
mount: `${normalizedName}.actor`,
autoStart: true,
},
{
type: elementClassName,
export: elementClassName,
mount: `${normalizedName}.element`,
autoStart: true,
},
],
permissions: { fsPolicy: 'none' },
};
const validation = validateMatrixManifest(manifest);
if (!validation.valid) {
throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`);
}
writeJsonFile(path.join(rootDir, 'matrix.json'), manifest);
writeJsonFile(path.join(rootDir, 'package.json'), generatePackageJson(normalizedName));
writeJsonFile(path.join(rootDir, 'tsconfig.json'), generateTsconfig());
fs.writeFileSync(path.join(rootDir, 'src', `${actorClassName}.ts`), actorTemplate(actorClassName, matrixActorImport), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', `${elementClassName}.ts`), elementTemplate(elementClassName, matrixActorImport), 'utf8');
fs.writeFileSync(
path.join(rootDir, 'src', 'index.ts'),
`export { ${actorClassName} } from './${actorClassName}.js';\nexport { ${elementClassName} } from './${elementClassName}.js';\n`,
'utf8'
);
writeJsonFile(path.join(rootDir, 'examples', 'query-value.mxq'), {
label: 'Query actor value',
operation: 'getValue',
payload: {},
});
writeJsonFile(path.join(rootDir, 'examples', 'render-element.mxq'), {
label: 'Render element',
operation: 'render',
payload: {},
});
fs.writeFileSync(
path.join(rootDir, 'skills', `${normalizedName}.skill.md`),
['---', `name: ${normalizedName}`, 'intents:', ' - action: query', ' operation: getValue', '---', '', '# actor-element skill', ''].join('\n'),
'utf8'
);
console.log(`Created actor-element scaffold at ${rootDir}`);
return { rootDir, actorClassName, elementClassName };
}

View File

@ -0,0 +1,605 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.js';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import { NatsTransport } from '@open-matrix/core/transport/NatsTransport.js';
import { connect as natsConnect, jwtAuthenticator, type NatsConnection } from 'nats';
import { loadMatrixServiceManifest, type ILoadedMatrixServiceManifest } from '../utils/service-manifest.js';
import { createRunnerSecurityRealm } from '../utils/runner-security.js';
export interface IActorRunCommandOptions {
readonly natsUrl?: string;
readonly root?: string;
readonly space?: string;
readonly jwt?: string;
readonly seed?: string;
readonly home?: string;
readonly export?: string;
readonly mount?: string;
readonly json?: boolean;
readonly props?: string;
readonly checkOp?: string;
readonly checkPayload?: string;
}
export interface IBrokerRunConfig {
readonly natsUrl: string;
readonly root: string;
readonly jwt?: string;
readonly seed?: string;
readonly checked: readonly string[];
}
type ActorConstructor = {
new (): MatrixActor;
readonly accepts?: Record<string, unknown>;
readonly emits?: Record<string, unknown>;
readonly subscribes?: Record<string, unknown>;
readonly name: string;
};
type PackageBootstrapFunction = (...args: readonly unknown[]) => Promise<unknown> | unknown;
export interface IPackageRunCommandOptions extends IActorRunCommandOptions {
readonly entry?: string;
readonly overrides?: string;
}
export interface IPackageRunSpec {
readonly packageDir: string;
readonly packageName: string;
readonly manifestPath: string;
readonly entryPath: string;
readonly exportName: string;
readonly mount: string;
readonly actorId: string;
readonly packageId: string;
readonly runtimeId: string;
readonly accepts: Record<string, unknown>;
readonly emits: Record<string, unknown>;
readonly subscribes: Record<string, unknown>;
readonly overrides: Record<string, unknown>;
readonly serviceInstance: ILoadedMatrixServiceManifest['serviceInstance'];
}
export async function actorRunCommand(
entry: string,
options: IActorRunCommandOptions,
): Promise<void> {
const entryPath = path.resolve(entry);
if (!fs.existsSync(entryPath)) {
throw new Error(`Actor entry not found: ${entryPath}`);
}
const exportName = options.export?.trim() || 'default';
const ActorClass = await loadActorConstructor(entryPath, exportName);
const mount = options.mount?.trim() || inferMountFromEntry(entryPath);
if (!mount) {
throw new Error('matrix actor run requires --mount <mount> when it cannot infer a mount from the entry file');
}
const broker = resolveActorRunBroker(options);
const connection = await connectActorNats(broker);
const transport = new NatsTransport(connection, { root: broker.root });
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
});
const props = parseProps(options.props);
await runtime.createSupervised(ActorClass, mount, props);
await transport.flush();
const check = await runOptionalActorCheck(runtime, mount, options);
const result = {
ok: true,
mode: 'standalone-actor-runner',
entry: entryPath,
export: exportName,
actor: ActorClass.name,
mount,
root: broker.root,
effectiveMount: `${broker.root}.${mount}`,
natsUrl: broker.natsUrl,
authMode: broker.jwt ? 'jwt' : 'none',
hostStateWritten: false,
...(check ? { check } : {}),
};
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log('Actor mounted:');
console.log(` logical: ${result.mount}`);
console.log(` root: ${result.root}`);
console.log(` effective: ${result.effectiveMount}`);
console.log(` broker: ${result.natsUrl}`);
console.log('Press Ctrl+C to stop.');
}
if (check) {
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
return;
}
await waitForActorShutdown(async () => {
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
});
}
export async function packageRunCommand(
packageDir: string,
options: IPackageRunCommandOptions,
): Promise<void> {
const spec = resolvePackageRunSpec(packageDir, options);
const broker = resolveActorRunBroker(options);
const connection = await connectActorNats(broker);
const transport = new NatsTransport(connection, { root: broker.root });
const runtime = new MatrixRuntime({
transport,
logging: false,
allowDynamicCompilation: false,
securityRealm: createRunnerSecurityRealm(),
});
const bootstrap = await loadPackageBootstrap(spec.entryPath, spec.exportName);
const bootstrapResult = await bootstrap(
spec.overrides,
{ runtime },
undefined,
{
packageName: spec.packageName,
packageDir: spec.packageDir,
manifestPath: spec.manifestPath,
entryPath: spec.entryPath,
exportName: spec.exportName,
root: broker.root,
mount: spec.mount,
instance: spec.serviceInstance,
serviceInstance: spec.serviceInstance,
},
);
await transport.flush();
const check = await runOptionalActorCheck(runtime, spec.mount, options);
const result = {
ok: true,
mode: 'standalone-package-runner',
packageDir: spec.packageDir,
packageName: spec.packageName,
manifest: spec.manifestPath,
entry: spec.entryPath,
export: spec.exportName,
mount: spec.mount,
root: broker.root,
effectiveMount: `${broker.root}.${spec.mount}`,
natsUrl: broker.natsUrl,
authMode: broker.jwt ? 'jwt' : 'none',
hostStateWritten: false,
...(check ? { check } : {}),
};
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log('Package actor mounted:');
console.log(` package: ${result.packageName}`);
console.log(` logical: ${result.mount}`);
console.log(` root: ${result.root}`);
console.log(` effective: ${result.effectiveMount}`);
console.log(` broker: ${result.natsUrl}`);
console.log('Press Ctrl+C to stop.');
}
if (check) {
await shutdownPackageBootstrapResult(bootstrapResult, runtime);
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
return;
}
await waitForActorShutdown(async () => {
await shutdownPackageBootstrapResult(bootstrapResult, runtime);
await runtime.shutdown();
await transport.disconnect();
if (!connection.isClosed()) {
await connection.close();
}
});
}
export function resolveActorRunBroker(options: IActorRunCommandOptions): IBrokerRunConfig {
const candidateHomes = resolveCandidateHomes(options.home);
const files = candidateHomes.flatMap((home) => [
{
label: `${formatHomeLabel(home)}/credentials/matrix-broker.json`,
value: readBrokerCredentialFile(path.join(home, 'credentials', 'matrix-broker.json')),
},
{
label: `${formatHomeLabel(home)}/credentials/matrix-nats.json`,
value: readBrokerCredentialFile(path.join(home, 'credentials', 'matrix-nats.json')),
},
{
label: `${formatHomeLabel(home)}/config.json`,
value: readBrokerConfigFile(path.join(home, 'config.json')),
},
]);
const natsUrl = readOptionalString(options.natsUrl)
?? readOptionalString(process.env.MATRIX_NATS_URL)
?? readOptionalString(process.env.NATS_URL)
?? firstConfigured(files, 'natsUrl');
const root = readOptionalString(options.root)
?? readOptionalString(options.space)
?? readOptionalString(process.env.MATRIX_ROOT)
?? readOptionalString(process.env.MATRIX_SPACE)
?? firstConfigured(files, 'root')
?? firstConfigured(files, 'space');
const jwt = readOptionalString(options.jwt)
?? readOptionalString(process.env.MATRIX_NATS_JWT)
?? readOptionalString(process.env.NATS_JWT)
?? firstConfigured(files, 'jwt');
const seed = readOptionalString(options.seed)
?? readOptionalString(process.env.MATRIX_NATS_SEED)
?? readOptionalString(process.env.NATS_SEED)
?? firstConfigured(files, 'seed');
const checked = [
'--nats-url',
'--root',
'--space',
'--jwt',
'--seed',
'MATRIX_NATS_URL',
'MATRIX_ROOT',
'MATRIX_SPACE',
'MATRIX_NATS_JWT',
'MATRIX_NATS_SEED',
'NATS_URL',
'NATS_JWT',
'NATS_SEED',
...files.map((entry) => entry.label),
];
if (!natsUrl) {
throw new Error(`MATRIX_NATS_URL_REQUIRED: matrix actor/package run requires --nats-url or configured broker URL. Checked: ${checked.join(', ')}`);
}
if (!root) {
throw new Error('MATRIX_ROOT_REQUIRED: matrix actor/package run requires --root, --space, MATRIX_ROOT, MATRIX_SPACE, or configured root');
}
if ((jwt && !seed) || (!jwt && seed)) {
throw new Error('MATRIX_NATS_JWT_SEED_PAIR_REQUIRED: provide both JWT and seed, or neither for an unauthenticated local broker');
}
return {
natsUrl,
root,
...(jwt ? { jwt } : {}),
...(seed ? { seed } : {}),
checked,
};
}
export function resolvePackageRunSpec(packageDir: string, options: IPackageRunCommandOptions): IPackageRunSpec {
const loaded = loadMatrixServiceManifest(packageDir);
const entryPath = options.entry
? resolvePackageEntryPath(loaded.packageDir, options.entry)
: loaded.entryPath;
const exportName = options.export?.trim() || loaded.exportName;
const mount = options.mount?.trim() || loaded.mount;
if (!mount) {
throw new Error('matrix package run requires --mount <mount> when the package descriptor does not declare one');
}
const binding = resolvePackageBindingForMount(loaded, mount);
const componentType = binding?.type ?? loaded.serviceInstance.componentType ?? loaded.serviceInstance.class;
const actorId = componentType || mount;
const runtimeId = `standalone-package:${process.pid}:${sanitizeRuntimeIdPart(loaded.packageName)}:${sanitizeRuntimeIdPart(mount)}`;
return {
packageDir: loaded.packageDir,
packageName: loaded.packageName,
manifestPath: loaded.manifestPath,
entryPath,
exportName,
mount,
actorId,
packageId: loaded.packageName,
runtimeId,
accepts: acceptsArrayToRecord(binding?.accepts ?? []),
emits: {},
subscribes: {},
overrides: parseOverrides(options.overrides),
serviceInstance: loaded.serviceInstance,
};
}
async function connectActorNats(input: IBrokerRunConfig): Promise<NatsConnection> {
const encoder = new TextEncoder();
return await natsConnect({
servers: [input.natsUrl],
name: `matrix-actor-run-${input.root}-${process.pid}`,
timeout: 5_000,
...(input.jwt && input.seed
? {
authenticator: jwtAuthenticator(
() => input.jwt!,
() => encoder.encode(input.seed!),
),
}
: {}),
});
}
export async function loadActorConstructor(entryPath: string, exportName: string): Promise<ActorConstructor> {
const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_actor_run=${Date.now()}-${Math.random().toString(16).slice(2)}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const requested = actorConstructorFromExport(mod[exportName]);
if (requested) {
return requested;
}
const available = Object.entries(mod)
.filter(([, value]) => actorConstructorFromExport(value))
.map(([name]) => name);
throw new Error(`No actor class found as export "${exportName}". Available exports: ${available.join(', ') || '(none)'}`);
}
async function loadPackageBootstrap(entryPath: string, exportName: string): Promise<PackageBootstrapFunction> {
const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_package_run=${Date.now()}-${Math.random().toString(16).slice(2)}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const bootstrap = mod[exportName];
if (typeof bootstrap !== 'function') {
throw new Error(`Package run export "${exportName}" was not found in ${entryPath}`);
}
return bootstrap as PackageBootstrapFunction;
}
function actorConstructorFromExport(value: unknown): ActorConstructor | null {
if (typeof value !== 'function') {
return null;
}
const candidate = value as ActorConstructor;
return candidate.prototype ? candidate : null;
}
function inferMountFromEntry(entryPath: string): string {
return path.basename(entryPath, path.extname(entryPath))
.replace(/Actor$/i, '')
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function parseProps(value: string | undefined): Record<string, unknown> | undefined {
const text = value?.trim();
if (!text) {
return undefined;
}
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('matrix actor run --props must be a JSON object');
}
return parsed as Record<string, unknown>;
}
function parseOverrides(value: string | undefined): Record<string, unknown> {
const text = value?.trim();
if (!text) {
return {};
}
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('matrix package run --overrides must be a JSON object');
}
return parsed as Record<string, unknown>;
}
async function runOptionalActorCheck(
runtime: MatrixRuntime,
mount: string,
options: IActorRunCommandOptions,
): Promise<{ readonly op: string; readonly result: unknown } | null> {
const op = options.checkOp?.trim();
if (!op) {
return null;
}
const payload = parseCheckPayload(options.checkPayload);
const result = await RequestReply.execute<Record<string, unknown>, unknown>(
runtime.getRootContext(),
mount,
op,
payload,
{ timeoutMs: 5_000 },
);
return { op, result };
}
function parseCheckPayload(value: string | undefined): Record<string, unknown> {
const text = value?.trim();
if (!text) {
return {};
}
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('matrix actor/package run --check-payload must be a JSON object');
}
return parsed as Record<string, unknown>;
}
function resolvePackageEntryPath(packageDir: string, entry: string): string {
const trimmed = entry.trim();
if (!trimmed) {
throw new Error('matrix package run --entry must be a non-empty path');
}
const entryPath = path.resolve(packageDir, trimmed);
if (!fs.existsSync(entryPath)) {
throw new Error(`matrix package run entry does not exist: ${trimmed}`);
}
return entryPath;
}
function resolvePackageBindingForMount(
loaded: ILoadedMatrixServiceManifest,
mount: string,
): ILoadedMatrixServiceManifest['packageComponents'][number] | ILoadedMatrixServiceManifest['packageRoot'] | undefined {
if (loaded.packageRoot?.mount === mount) {
return loaded.packageRoot;
}
return loaded.packageComponents.find((entry) => entry.mount === mount);
}
function acceptsArrayToRecord(accepts: readonly string[]): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const op of accepts) {
result[op] = {};
}
return result;
}
function sanitizeRuntimeIdPart(value: string): string {
return value
.replace(/^@/u, '')
.replace(/[^a-zA-Z0-9._-]+/gu, '-')
.replace(/-+/gu, '-')
.replace(/^-|-$/gu, '')
|| 'package';
}
async function shutdownPackageBootstrapResult(value: unknown, ownedRuntime: MatrixRuntime): Promise<void> {
if (!value || typeof value !== 'object') {
return;
}
const runtime = (value as { readonly runtime?: unknown }).runtime;
if (!runtime || typeof runtime !== 'object') {
return;
}
if (runtime === ownedRuntime) {
return;
}
const shutdown = (runtime as { readonly shutdown?: unknown }).shutdown;
if (typeof shutdown === 'function') {
await shutdown.call(runtime);
}
}
function waitForActorShutdown(onShutdown: () => Promise<void>): Promise<void> {
return new Promise<void>((resolve, reject) => {
let settled = false;
const finish = (error?: unknown): void => {
if (settled) return;
settled = true;
process.off('SIGINT', shutdown);
process.off('SIGTERM', shutdown);
if (error) {
reject(error);
return;
}
resolve();
};
const shutdown = (): void => {
void onShutdown().then(() => finish()).catch((error) => finish(error));
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
});
}
function resolveCandidateHomes(homeOption: string | undefined): readonly string[] {
const homes = [
homeOption ? path.resolve(homeOption) : undefined,
path.resolve(process.cwd(), '.matrix'),
path.resolve(os.homedir(), '.matrix'),
].filter((value): value is string => Boolean(value));
return [...new Set(homes)];
}
function formatHomeLabel(home: string): string {
const defaultHome = path.resolve(os.homedir(), '.matrix');
const cwdHome = path.resolve(process.cwd(), '.matrix');
if (home === defaultHome) return '~/.matrix';
if (home === cwdHome) return './.matrix';
return home;
}
function readBrokerCredentialFile(filePath: string): Record<string, string> {
const value = readJsonFile(filePath);
return normalizeBrokerConfig(value);
}
function readBrokerConfigFile(filePath: string): Record<string, string> {
const value = readJsonFile(filePath);
const nested = [
value,
value?.broker,
value?.nats,
value?.transport,
value?.credentials,
].filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === 'object' && !Array.isArray(entry)));
return nested.reduce<Record<string, string>>((result, entry) => ({
...result,
...normalizeBrokerConfig(entry),
}), {});
}
function normalizeBrokerConfig(value: Record<string, unknown> | null): Record<string, string> {
if (!value) {
return {};
}
return {
...(readOptionalString(value.natsUrl) ?? readOptionalString(value.url) ?? readOptionalString(value.brokerUrl) ? { natsUrl: (readOptionalString(value.natsUrl) ?? readOptionalString(value.url) ?? readOptionalString(value.brokerUrl))! } : {}),
...(readOptionalString(value.root) ?? readOptionalString(value.space) ?? readOptionalString(value.authorityRoot) ? { root: (readOptionalString(value.root) ?? readOptionalString(value.space) ?? readOptionalString(value.authorityRoot))! } : {}),
...(readOptionalString(value.space) ? { space: readOptionalString(value.space)! } : {}),
...(readOptionalString(value.jwt) ? { jwt: readOptionalString(value.jwt)! } : {}),
...(readOptionalString(value.seed) ? { seed: readOptionalString(value.seed)! } : {}),
};
}
function firstConfigured(
files: readonly { readonly value: Record<string, string> }[],
key: 'natsUrl' | 'root' | 'space' | 'jwt' | 'seed',
): string | undefined {
for (const file of files) {
const value = file.value[key];
if (value) {
return value;
}
}
return undefined;
}
function readJsonFile(filePath: string): Record<string, unknown> | null {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid JSON in ${filePath}: ${message}`);
}
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}

View File

@ -0,0 +1,236 @@
/**
* mx actor command implementation.
*
* Scaffolds a complete actor package with TypeScript, tests, examples, and skills.
* Implementation: B014
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { toKebabCase, toPascalCase, validateMatrixManifest, writeJsonFile, generatePackageJson, generateTsconfig } from '../utils/scaffold.js';
export interface IActorCommandOptions {
readonly cwd?: string;
readonly outputDir?: string;
}
export interface IActorCommandResult {
readonly rootDir: string;
readonly className: string;
readonly mount: string;
readonly workspacePath?: string;
}
function actorTemplate(className: string, matrixActorImport: string): string {
return [
`import { MatrixActor } from ${JSON.stringify(matrixActorImport)};`,
'',
`export class ${className} extends MatrixActor {`,
' static override accepts = {',
" getStatus: {},",
' };',
'',
' static override emits = {',
" statusChanged: { status: 'string' },",
' };',
'',
" private _status = 'ready';",
'',
' protected onGetStatus(): { status: string } {',
' return { status: this._status };',
' }',
'}',
'',
].join('\n');
}
function actorIndexTemplate(className: string): string {
return [
`export { ${className} } from './${className}.js';`,
`export { createStandalone${className} } from './create-standalone-${className}.js';`,
'',
].join('\n');
}
function actorFactoryTemplate(className: string, mount: string, matrixActorImport: string): string {
const factoryName = `createStandalone${className}`;
const actorFile = `./${className}.js`;
return [
`import { InMemoryBroker, InMemoryTransport, MatrixRuntime } from ${JSON.stringify(matrixActorImport)};`,
'',
`import { ${className} } from ${JSON.stringify(actorFile)};`,
'',
'interface IStandaloneBootContext {',
' readonly root?: string;',
' readonly mount?: string;',
'}',
'',
'interface IStandaloneRunnerServices {',
' readonly runtime?: MatrixRuntime;',
'}',
'',
'function readTrimmedString(value: unknown): string | undefined {',
" if (typeof value !== 'string') {",
' return undefined;',
' }',
' const trimmed = value.trim();',
' return trimmed.length > 0 ? trimmed : undefined;',
'}',
'',
'function requiredStandaloneRoot(requestedRoot: string | undefined, runtime: MatrixRuntime): string {',
' const runtimeRoot = readTrimmedString(runtime.transport.root);',
' const root = requestedRoot ?? runtimeRoot;',
' if (!root) {',
` throw new Error('${factoryName} requires bootContext.root or a runtime transport root');`,
' }',
' return root;',
'}',
'',
`export async function ${factoryName}(`,
' _overrides: Record<string, unknown> = {},',
' services?: IStandaloneRunnerServices,',
' _config?: unknown,',
' bootContext: IStandaloneBootContext = {},',
'): Promise<{',
' runtime: MatrixRuntime;',
` actor: ${className};`,
' root: string;',
' mount: string;',
'}> {',
' const requestedRoot = readTrimmedString(bootContext.root);',
' const runtime = services?.runtime ?? new MatrixRuntime({',
' transport: new InMemoryTransport(new InMemoryBroker(), {',
` name: ${JSON.stringify(`${mount}-standalone`)},`,
' ...(requestedRoot ? { root: requestedRoot } : {}),',
' }),',
' logging: false,',
' });',
' const root = requiredStandaloneRoot(requestedRoot, runtime);',
` const mount = readTrimmedString(bootContext.mount) ?? ${JSON.stringify(mount)};`,
` const actor = await runtime.createSupervised(${className}, mount);`,
' return { runtime, actor, root, mount };',
'}',
'',
].join('\n');
}
function actorFactoryManifest(className: string, mount: string): Record<string, unknown> {
return {
kind: 'factory',
export: `createStandalone${className}`,
instance: {
id: `RUNTIME-HOST-${mount.toUpperCase().replace(/[^A-Z0-9]+/g, '-')}`,
class: 'service',
rootKind: 'component',
componentType: className,
autoStart: true,
},
};
}
function actorTestTemplate(className: string): string {
return [
"import { describe, it } from 'node:test';",
"import { strict as assert } from 'node:assert';",
`import { ${className} } from '../src/${className}.ts';`,
'',
`describe('${className}', () => {`,
" it('declares getStatus command', () => {",
` assert.ok(Object.prototype.hasOwnProperty.call(${className}.accepts, 'getStatus'));`,
' });',
'});',
'',
].join('\n');
}
function actorSkillTemplate(name: string, className: string): string {
return [
'---',
`name: ${name}`,
'intents:',
' - action: query',
' dataType: status',
' category: system',
'---',
'',
`# ${className}`,
'',
'Returns actor status for orchestration and health checks.',
'',
].join('\n');
}
export async function actorCommand(name: string, options: IActorCommandOptions = {}): Promise<IActorCommandResult> {
const normalizedName = toKebabCase(name);
if (!normalizedName) {
throw new Error('Actor name must not be empty');
}
const className = `${toPascalCase(normalizedName)}Actor`;
const mount = normalizedName;
const cwd = path.resolve(options.cwd ?? process.cwd());
const rootDir = path.resolve(options.outputDir ?? path.join(cwd, normalizedName));
const matrixActorImport = '@open-matrix/core';
fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'examples'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'skills'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'tests'), { recursive: true });
const manifest = {
name: `@matrix/${normalizedName}`,
version: '0.1.0',
description: `${className} generated by mx actor`,
runtime: {
language: 'typescript',
entry: 'dist/index.js',
factory: actorFactoryManifest(className, mount),
},
components: [
{
type: className,
export: className,
mount,
surface: 'headless',
accepts: ['getStatus'],
emits: ['statusChanged'],
},
],
permissions: { fsPolicy: 'none' },
};
const validation = validateMatrixManifest(manifest);
if (!validation.valid) {
throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`);
}
writeJsonFile(path.join(rootDir, 'matrix.json'), manifest);
writeJsonFile(path.join(rootDir, 'package.json'), generatePackageJson(normalizedName, { service: true }));
writeJsonFile(path.join(rootDir, 'tsconfig.json'), generateTsconfig({ service: true }));
fs.writeFileSync(
path.join(rootDir, 'src', `${className}.ts`),
actorTemplate(className, matrixActorImport),
'utf8'
);
fs.writeFileSync(
path.join(rootDir, 'src', `create-standalone-${className}.ts`),
actorFactoryTemplate(className, mount, matrixActorImport),
'utf8'
);
fs.writeFileSync(path.join(rootDir, 'src', 'index.ts'), actorIndexTemplate(className), 'utf8');
writeJsonFile(path.join(rootDir, 'examples', 'get-status.mxq'), {
label: 'Get Status',
operation: 'getStatus',
payload: {},
description: `Runs ${className}.getStatus`,
});
fs.writeFileSync(
path.join(rootDir, 'skills', `${normalizedName}.skill.md`),
actorSkillTemplate(normalizedName, className),
'utf8'
);
fs.writeFileSync(path.join(rootDir, 'tests', `${normalizedName}.spec.ts`), actorTestTemplate(className), 'utf8');
console.log(`Created actor scaffold at ${rootDir}`);
return { rootDir, className, mount };
}

View File

@ -0,0 +1,95 @@
import {
readRegistryCredential,
removeRegistryCredential,
saveRegistryCredential,
type IRegistryCredential,
} from '../utils/auth-store.js';
export interface IAuthContextOptions {
readonly cwd?: string;
readonly registry?: string;
readonly credentialsPath?: string;
}
export interface ILoginCommandOptions extends IAuthContextOptions {
readonly username: string;
readonly token: string;
readonly email?: string;
readonly expiresAt?: string;
}
export interface ILoginCommandResult {
readonly registry: string;
readonly username: string;
readonly credentialsPath: string;
}
export interface ILogoutCommandResult {
readonly registry: string;
readonly removed: boolean;
readonly credentialsPath: string;
}
export interface IWhoamiCommandResult {
readonly registry: string;
readonly authenticated: boolean;
readonly username?: string;
readonly email?: string;
readonly expiresAt?: string;
}
export async function loginCommand(options: ILoginCommandOptions): Promise<ILoginCommandResult> {
if (!options.username.trim()) {
throw new Error('MX_AUTH_REQUIRED: username is required for login');
}
if (!options.token.trim()) {
throw new Error('MX_AUTH_REQUIRED: token is required for login');
}
const cwd = options.cwd ?? process.cwd();
const credential: IRegistryCredential = {
username: options.username.trim(),
token: options.token.trim(),
email: options.email?.trim() || undefined,
expiresAt: options.expiresAt?.trim() || undefined,
};
const { registry, credentialsPath } = saveRegistryCredential(cwd, credential, {
registry: options.registry,
credentialsPath: options.credentialsPath,
});
return {
registry,
username: credential.username,
credentialsPath,
};
}
export async function logoutCommand(options: IAuthContextOptions = {}): Promise<ILogoutCommandResult> {
const cwd = options.cwd ?? process.cwd();
return removeRegistryCredential(cwd, {
registry: options.registry,
credentialsPath: options.credentialsPath,
});
}
export async function whoamiCommand(options: IAuthContextOptions = {}): Promise<IWhoamiCommandResult> {
const cwd = options.cwd ?? process.cwd();
const { registry, credential } = readRegistryCredential(cwd, {
registry: options.registry,
credentialsPath: options.credentialsPath,
});
if (!credential) {
return { registry, authenticated: false };
}
return {
registry,
authenticated: true,
username: credential.username,
email: credential.email,
expiresAt: credential.expiresAt,
};
}

View File

@ -0,0 +1,93 @@
import {
readMatrixCliConfig,
resetMatrixCliConfig,
resolvePackageRegistry,
setMatrixCliConfigValue,
type IMatrixCliConfig,
} from '../utils/config-store.js';
export interface IConfigCommandOptions {
readonly cwd?: string;
readonly configPath?: string;
}
export interface IConfigGetResult {
readonly key: string;
readonly value: string | undefined;
readonly source?: 'explicit' | 'env' | 'config' | 'default';
readonly configPath: string;
}
export interface IConfigListResult {
readonly configPath: string;
readonly values: IMatrixCliConfig;
readonly effective: {
readonly registry: string;
readonly registrySource: 'explicit' | 'env' | 'config' | 'default';
};
}
export interface IConfigSetResult {
readonly key: string;
readonly value: string;
readonly configPath: string;
}
export interface IConfigResetResult {
readonly configPath: string;
readonly values: IMatrixCliConfig;
}
function assertSupportedKey(key: string): 'registry' {
const normalized = key.trim();
if (normalized !== 'registry') {
throw new Error(`MX_CONFIG_INVALID: unsupported config key "${key}"`);
}
return normalized;
}
export async function configGetCommand(
key: string,
options: IConfigCommandOptions = {},
): Promise<IConfigGetResult> {
const normalizedKey = assertSupportedKey(key);
const stored = readMatrixCliConfig(options);
const effective = resolvePackageRegistry(undefined, options);
return {
key: normalizedKey,
value: stored.values.registry ?? effective.registry,
source: stored.values.registry ? 'config' : effective.source,
configPath: stored.configPath,
};
}
export async function configListCommand(options: IConfigCommandOptions = {}): Promise<IConfigListResult> {
const stored = readMatrixCliConfig(options);
const effective = resolvePackageRegistry(undefined, options);
return {
configPath: stored.configPath,
values: stored.values,
effective: {
registry: effective.registry,
registrySource: effective.source,
},
};
}
export async function configSetCommand(
key: string,
value: string,
options: IConfigCommandOptions = {},
): Promise<IConfigSetResult> {
const normalizedKey = assertSupportedKey(key);
const result = setMatrixCliConfigValue(normalizedKey, value, options);
return {
key: normalizedKey,
value: result.values.registry ?? '',
configPath: result.configPath,
};
}
export async function configResetCommand(options: IConfigCommandOptions = {}): Promise<IConfigResetResult> {
return resetMatrixCliConfig(options);
}

View File

@ -0,0 +1,114 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { toKebabCase, toPascalCase, validateMatrixManifest, writeJsonFile, generatePackageJson, generateTsconfig } from '../utils/scaffold.js';
export interface ICqrsCommandOptions {
readonly cwd?: string;
readonly outputDir?: string;
}
export interface ICqrsCommandResult {
readonly rootDir: string;
readonly commandClassName: string;
readonly queryClassName: string;
}
function actorTemplate(className: string, operation: string, matrixActorImport: string): string {
return [
`import { MatrixActor } from ${JSON.stringify(matrixActorImport)};`,
'',
`export class ${className} extends MatrixActor {`,
' static override accepts = {',
` ${operation}: {},`,
' };',
'',
` protected on${operation.charAt(0).toUpperCase()}${operation.slice(1)}(): { ok: boolean } {`,
' return { ok: true };',
' }',
'}',
'',
].join('\n');
}
export async function cqrsCommand(name: string, options: ICqrsCommandOptions = {}): Promise<ICqrsCommandResult> {
const normalizedName = toKebabCase(name);
if (!normalizedName) {
throw new Error('cqrs name must not be empty');
}
const baseName = toPascalCase(normalizedName);
const commandClassName = `${baseName}CommandActor`;
const queryClassName = `${baseName}QueryActor`;
const cwd = path.resolve(options.cwd ?? process.cwd());
const rootDir = path.resolve(options.outputDir ?? path.join(cwd, normalizedName));
const matrixActorImport = '@open-matrix/core';
fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'examples'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'skills'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'tests'), { recursive: true });
const manifest = {
name: normalizedName,
version: '0.1.0',
description: `${baseName} CQRS package generated by mx cqrs`,
runtime: { language: 'typescript', entry: 'dist/index.js' },
components: [
{
type: commandClassName,
export: commandClassName,
mount: `${normalizedName}.command`,
autoStart: true,
},
{
type: queryClassName,
export: queryClassName,
mount: `${normalizedName}.query`,
autoStart: true,
},
],
permissions: { fsPolicy: 'none' },
};
const validation = validateMatrixManifest(manifest);
if (!validation.valid) {
throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`);
}
writeJsonFile(path.join(rootDir, 'matrix.json'), manifest);
writeJsonFile(path.join(rootDir, 'package.json'), generatePackageJson(normalizedName));
writeJsonFile(path.join(rootDir, 'tsconfig.json'), generateTsconfig());
fs.writeFileSync(
path.join(rootDir, 'src', `${commandClassName}.ts`),
actorTemplate(commandClassName, 'execute', matrixActorImport),
'utf8'
);
fs.writeFileSync(
path.join(rootDir, 'src', `${queryClassName}.ts`),
actorTemplate(queryClassName, 'query', matrixActorImport),
'utf8'
);
fs.writeFileSync(
path.join(rootDir, 'src', 'index.ts'),
`export { ${commandClassName} } from './${commandClassName}.js';\nexport { ${queryClassName} } from './${queryClassName}.js';\n`,
'utf8'
);
writeJsonFile(path.join(rootDir, 'examples', 'execute.mxq'), {
label: 'Execute command',
operation: 'execute',
payload: {},
});
writeJsonFile(path.join(rootDir, 'examples', 'query.mxq'), {
label: 'Query projection',
operation: 'query',
payload: {},
});
fs.writeFileSync(
path.join(rootDir, 'skills', `${normalizedName}.skill.md`),
['---', `name: ${normalizedName}`, 'intents:', ' - action: command', ' operation: execute', '---', '', '# cqrs skill', ''].join('\n'),
'utf8'
);
console.log(`Created cqrs scaffold at ${rootDir}`);
return { rootDir, commandClassName, queryClassName };
}

View File

@ -0,0 +1,108 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { toKebabCase, toPascalCase, validateMatrixManifest, writeJsonFile, generatePackageJson, generateTsconfig } from '../utils/scaffold.js';
export interface IElementCommandOptions {
readonly cwd?: string;
readonly outputDir?: string;
}
export interface IElementCommandResult {
readonly rootDir: string;
readonly className: string;
readonly mount: string;
}
function elementTemplate(className: string, matrixActorImport: string): string {
return [
`import { MatrixActor } from ${JSON.stringify(matrixActorImport)};`,
'',
`export class ${className} extends MatrixActor {`,
' static override accepts = {',
" render: {},",
' };',
'',
' protected onRender(): { ok: boolean; kind: string } {',
" return { ok: true, kind: 'element' };",
' }',
'}',
'',
].join('\n');
}
export async function elementCommand(name: string, options: IElementCommandOptions = {}): Promise<IElementCommandResult> {
const normalizedName = toKebabCase(name);
if (!normalizedName) {
throw new Error('Element name must not be empty');
}
const className = `${toPascalCase(normalizedName)}Element`;
const mount = normalizedName;
const cwd = path.resolve(options.cwd ?? process.cwd());
const rootDir = path.resolve(options.outputDir ?? path.join(cwd, normalizedName));
const matrixActorImport = '@open-matrix/core';
fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'examples'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'skills'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'tests'), { recursive: true });
const manifest = {
name: normalizedName,
version: '0.1.0',
description: `${className} generated by mx element`,
runtime: { language: 'typescript', entry: 'dist/index.js' },
components: [
{
type: className,
export: className,
mount,
autoStart: true,
props: {
surface: 'element',
},
},
],
permissions: { fsPolicy: 'none' },
};
const validation = validateMatrixManifest(manifest);
if (!validation.valid) {
throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`);
}
writeJsonFile(path.join(rootDir, 'matrix.json'), manifest);
writeJsonFile(path.join(rootDir, 'package.json'), generatePackageJson(normalizedName));
writeJsonFile(path.join(rootDir, 'tsconfig.json'), generateTsconfig());
fs.writeFileSync(path.join(rootDir, 'src', `${className}.ts`), elementTemplate(className, matrixActorImport), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', 'index.ts'), `export { ${className} } from './${className}.js';\n`, 'utf8');
writeJsonFile(path.join(rootDir, 'examples', 'render.mxq'), {
label: 'Render Element',
operation: 'render',
payload: {},
});
fs.writeFileSync(
path.join(rootDir, 'skills', `${normalizedName}.skill.md`),
['---', `name: ${normalizedName}`, 'intents:', ' - action: render', ' operation: render', '---', '', `# ${className}`, ''].join('\n'),
'utf8'
);
fs.writeFileSync(
path.join(rootDir, 'tests', `${normalizedName}.spec.ts`),
[
"import { describe, it } from 'node:test';",
"import { strict as assert } from 'node:assert';",
`import { ${className} } from '../src/${className}.ts';`,
'',
`describe('${className}', () => {`,
" it('declares render command', () => {",
` assert.ok(Object.prototype.hasOwnProperty.call(${className}.accepts, 'render'));`,
' });',
'});',
'',
].join('\n'),
'utf8'
);
console.log(`Created element scaffold at ${rootDir}`);
return { rootDir, className, mount };
}

View File

@ -0,0 +1,254 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
packageNameToPath,
readVersionFromPackageDir,
resolvePackagesNodeModulesRoot,
} from '../utils/package-store.js';
import { computeDirectorySha256 } from '../utils/archive.js';
type JsonRecord = Record<string, unknown>;
interface IForkedFromRecord {
readonly origin: string;
readonly originVersion: string;
readonly forkedAt: string;
readonly forkedBy: string;
readonly originSha256: string;
}
interface INameRename {
readonly oldName: string;
readonly newName: string;
}
export interface IForkCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
readonly name?: string;
readonly replaceOrigin?: boolean;
readonly forkedBy?: string;
}
export interface IForkCommandResult {
readonly sourcePackageName: string;
readonly forkedPackageName: string;
readonly sourceDir: string;
readonly targetDir: string;
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function toPrefix(targetPackageName: string): { readonly mountPrefix: string; readonly typePrefix: string } {
const scoped = targetPackageName.startsWith('@')
? targetPackageName.slice(1).split('/')[0]
: targetPackageName.split('/')[0];
const mountPrefix = (scoped || 'fork').replace(/[^a-zA-Z0-9]+/g, '.').replace(/\.+/g, '.').replace(/^\./, '').toLowerCase();
const typePrefix = mountPrefix
.split('.')
.filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join('');
return {
mountPrefix: mountPrefix || 'fork',
typePrefix: typePrefix || 'Fork',
};
}
function renameType(original: unknown, typePrefix: string): string | undefined {
if (typeof original !== 'string') return undefined;
const trimmed = original.trim();
if (!trimmed) return undefined;
return trimmed.startsWith(typePrefix) ? trimmed : `${typePrefix}${trimmed}`;
}
function renameMount(original: unknown, mountPrefix: string): string | undefined {
if (typeof original !== 'string') return undefined;
const trimmed = original.trim();
if (!trimmed) return undefined;
return trimmed.startsWith(`${mountPrefix}.`) ? trimmed : `${mountPrefix}.${trimmed}`;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function deriveForkName(sourcePackageName: string): string {
if (sourcePackageName.startsWith('@')) {
const [, rawBase = 'package'] = sourcePackageName.split('/');
return `@fork/${rawBase}`;
}
return `${sourcePackageName}-fork`;
}
function readJson(filePath: string): JsonRecord {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
const record = asRecord(parsed);
if (!record) {
throw new Error(`Expected JSON object in ${filePath}`);
}
return record;
}
function writeJson(filePath: string, payload: JsonRecord): void {
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
}
function rewriteMatrixManifest(
targetDir: string,
targetPackageName: string,
replaceOrigin: boolean,
): ReadonlyArray<INameRename> {
const matrixJsonPath = path.join(targetDir, 'matrix.json');
if (!fs.existsSync(matrixJsonPath)) {
return [];
}
const manifest = readJson(matrixJsonPath);
const renames: INameRename[] = [];
manifest.name = targetPackageName;
if (!replaceOrigin && Array.isArray(manifest.components)) {
const { mountPrefix, typePrefix } = toPrefix(targetPackageName);
manifest.components = manifest.components.map((entry) => {
const component = asRecord(entry) ?? {};
const renamedType = renameType(component.type, typePrefix);
const renamedExport = renameType(component.export, typePrefix);
const renamedMount = renameMount(component.mount, mountPrefix);
if (typeof component.type === 'string' && renamedType && renamedType !== component.type) {
renames.push({ oldName: component.type, newName: renamedType });
}
if (typeof component.export === 'string' && renamedExport && renamedExport !== component.export) {
renames.push({ oldName: component.export, newName: renamedExport });
}
return {
...component,
type: renamedType ?? component.type,
export: renamedExport ?? component.export,
mount: renamedMount ?? component.mount,
};
});
}
writeJson(matrixJsonPath, manifest);
return renames;
}
function rewritePackageJsonName(targetDir: string, targetPackageName: string): void {
const packageJsonPath = path.join(targetDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return;
}
const packageJson = readJson(packageJsonPath);
packageJson.name = targetPackageName;
writeJson(packageJsonPath, packageJson);
}
function copyPackageTree(sourceDir: string, targetDir: string): void {
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
fs.cpSync(sourceDir, targetDir, {
recursive: true,
filter: (entry) => {
const base = path.basename(entry);
return base !== '.git' && base !== 'node_modules' && base !== '.forked-from';
},
});
}
function rewriteSourceExports(targetDir: string, renames: ReadonlyArray<INameRename>): void {
if (renames.length === 0) {
return;
}
const uniqueRenames = Array.from(
new Map(renames.map((entry) => [`${entry.oldName}=>${entry.newName}`, entry] as const)).values()
);
const filesToRewrite: string[] = [];
const queue = [targetDir];
while (queue.length > 0) {
const current = queue.pop()!;
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
if (entry.name === 'node_modules' || entry.name === '.git') {
continue;
}
const absolute = path.join(current, entry.name);
if (entry.isDirectory()) {
queue.push(absolute);
} else if (entry.isFile() && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
filesToRewrite.push(absolute);
}
}
}
for (const filePath of filesToRewrite) {
let content = fs.readFileSync(filePath, 'utf8');
let changed = false;
for (const rename of uniqueRenames) {
const pattern = new RegExp(`\\b${escapeRegExp(rename.oldName)}\\b`, 'g');
if (pattern.test(content)) {
content = content.replace(pattern, rename.newName);
changed = true;
}
}
if (changed) {
fs.writeFileSync(filePath, content, 'utf8');
}
}
}
function pruneIfEmpty(dirPath: string): void {
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
return;
}
if (fs.readdirSync(dirPath).length === 0) {
fs.rmdirSync(dirPath);
}
}
export async function forkCommand(sourcePackageName: string, options: IForkCommandOptions = {}): Promise<IForkCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
const sourceDir = path.join(nodeModulesRoot, packageNameToPath(sourcePackageName));
if (!fs.existsSync(sourceDir)) {
throw new Error(`MX_FORK_SOURCE_MISSING: ${sourcePackageName}`);
}
const targetPackageName = options.name?.trim() || deriveForkName(sourcePackageName);
const targetDir = path.join(nodeModulesRoot, packageNameToPath(targetPackageName));
if (fs.existsSync(targetDir)) {
throw new Error(`Target package already exists: ${targetPackageName}`);
}
copyPackageTree(sourceDir, targetDir);
rewritePackageJsonName(targetDir, targetPackageName);
const renames = rewriteMatrixManifest(targetDir, targetPackageName, Boolean(options.replaceOrigin));
if (!options.replaceOrigin) {
rewriteSourceExports(targetDir, renames);
}
const record: IForkedFromRecord = {
origin: sourcePackageName,
originVersion: readVersionFromPackageDir(sourceDir),
forkedAt: new Date().toISOString(),
forkedBy: options.forkedBy ?? process.env.USER ?? 'unknown',
originSha256: computeDirectorySha256(sourceDir),
};
fs.writeFileSync(path.join(targetDir, '.forked-from'), JSON.stringify(record, null, 2), 'utf8');
if (options.replaceOrigin && path.resolve(sourceDir) !== path.resolve(targetDir)) {
fs.rmSync(sourceDir, { recursive: true, force: true });
pruneIfEmpty(path.dirname(sourceDir));
}
return {
sourcePackageName,
forkedPackageName: targetPackageName,
sourceDir,
targetDir,
};
}

View File

@ -0,0 +1,38 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
packageNameToPath,
readManifestFromPackageDir,
resolvePackagesNodeModulesRoot,
} from '../utils/package-store.js';
export interface IInfoCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
}
export interface IInfoCommandResult {
readonly packageName: string;
readonly installPath: string;
readonly hasMatrixJson: boolean;
readonly manifest: Record<string, unknown>;
}
export async function infoCommand(packageName: string, options: IInfoCommandOptions = {}): Promise<IInfoCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
const installPath = path.join(nodeModulesRoot, packageNameToPath(packageName));
if (!fs.existsSync(installPath)) {
throw new Error(`Package is not installed: ${packageName}`);
}
const { packageName: resolvedName, manifest } = readManifestFromPackageDir(installPath);
const hasMatrixJson = fs.existsSync(path.join(installPath, 'matrix.json'));
return {
packageName: resolvedName,
installPath,
hasMatrixJson,
manifest,
};
}

View File

@ -0,0 +1,133 @@
/**
* mx init command implementation.
*
* Creates matrix.json and scaffolds directory structure.
* Implementation: B013
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { validateMatrixManifest, writeJsonFile } from '../utils/scaffold.js';
export interface IInitCommandOptions {
readonly cwd?: string;
readonly name?: string;
readonly version?: string;
readonly description?: string;
readonly yes?: boolean;
}
export interface IInitCommandResult {
readonly rootDir: string;
readonly manifestPath: string;
}
function defaultPackageName(cwd: string): string {
return path.basename(cwd) || 'matrix-package';
}
async function promptIfNeeded(
question: string,
fallback: string,
enabled: boolean,
rl: readline.Interface
): Promise<string> {
if (!enabled) {
return fallback;
}
const suffix = fallback.length > 0 ? ` (${fallback})` : '';
const answer = (await rl.question(`${question}${suffix}: `)).trim();
return answer.length > 0 ? answer : fallback;
}
export async function initCommand(options: IInitCommandOptions = {}): Promise<IInitCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const shouldPrompt = options.yes !== true;
const rl = readline.createInterface({ input, output });
try {
const name = await promptIfNeeded(
'Package name',
options.name ?? defaultPackageName(cwd),
shouldPrompt,
rl
);
const version = await promptIfNeeded(
'Version',
options.version ?? '0.1.0',
shouldPrompt,
rl
);
const description = await promptIfNeeded(
'Description',
options.description ?? '',
shouldPrompt,
rl
);
const manifest = {
name,
version,
description,
runtime: { language: 'typescript', entry: 'dist/index.js' },
components: [],
permissions: { fsPolicy: 'none' },
};
const validation = validateMatrixManifest(manifest);
if (!validation.valid) {
throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`);
}
fs.mkdirSync(path.join(cwd, 'src'), { recursive: true });
fs.mkdirSync(path.join(cwd, 'examples'), { recursive: true });
fs.mkdirSync(path.join(cwd, 'skills'), { recursive: true });
fs.mkdirSync(path.join(cwd, 'tests'), { recursive: true });
const manifestPath = path.join(cwd, 'matrix.json');
writeJsonFile(manifestPath, manifest);
const srcIndex = path.join(cwd, 'src', 'index.ts');
if (!fs.existsSync(srcIndex)) {
fs.writeFileSync(
srcIndex,
[
'// Entry point for your Matrix package.',
'// Add exports for your actors/services here.',
'export {};',
'',
].join('\n'),
'utf8'
);
}
const testScaffold = path.join(cwd, 'tests', 'package.spec.ts');
if (!fs.existsSync(testScaffold)) {
fs.writeFileSync(
testScaffold,
[
"import { describe, it } from 'node:test';",
"import { strict as assert } from 'node:assert';",
'',
"describe('package scaffold', () => {",
" it('has a valid matrix manifest placeholder', () => {",
" assert.equal(true, true);",
' });',
'});',
'',
].join('\n'),
'utf8'
);
}
console.log('Created matrix.json');
console.log('Created src/, examples/, skills/, and tests/');
return { rootDir: cwd, manifestPath };
} finally {
rl.close();
}
}

View File

@ -0,0 +1,676 @@
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
packageNameToPath,
resolveRegistryRoot,
readManifestFromPackageDir,
readVersionFromPackageDir,
resolvePackagesNodeModulesRoot,
validateResolvedManifest,
} from '../utils/package-store.js';
import { extractTarball } from '../utils/archive.js';
import { resolveRegistryTarball } from '../utils/registry-store.js';
import {
MATRIX_NPM_REGISTRY_SCOPES,
npmPackageSpecifier,
prepareNpmRegistryTarball,
} from '../utils/npm-registry-client.js';
import { resolvePackageRegistry } from '../utils/config-store.js';
import { readRegistryCredential } from '../utils/auth-store.js';
export interface IInstallCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
readonly registryDir?: string;
readonly registry?: string;
readonly credentialsPath?: string;
readonly force?: boolean;
}
export interface IInstallCommandResult {
readonly packageName: string;
readonly source: string;
readonly version: string;
readonly targetDir: string;
readonly lifecyclePhases: readonly string[];
}
type JsonRecord = Record<string, unknown>;
type LifecyclePhase = 'validate' | 'migrate' | 'seed' | 'verify';
interface DependencyInstallOptions {
readonly install: boolean;
readonly registry?: string;
readonly credentialsPath?: string;
readonly cwd: string;
}
interface LifecycleHook {
readonly script: string;
readonly description?: string;
}
interface InstallLifecycle {
readonly validate?: LifecycleHook;
readonly migrate?: LifecycleHook;
readonly seed?: LifecycleHook;
readonly verify?: LifecycleHook;
}
interface RuntimeNpmDependencies {
readonly dependencies?: JsonRecord;
readonly optionalDependencies?: JsonRecord;
}
function parsePackageSpecifier(specifier: string): { packageName: string; version?: string } {
if (specifier.startsWith('@')) {
const slashIndex = specifier.indexOf('/');
if (slashIndex === -1) {
return { packageName: specifier };
}
const atIndex = specifier.lastIndexOf('@');
if (atIndex > slashIndex) {
return {
packageName: specifier.slice(0, atIndex),
version: specifier.slice(atIndex + 1),
};
}
return { packageName: specifier };
}
const atIndex = specifier.lastIndexOf('@');
if (atIndex > 0) {
return {
packageName: specifier.slice(0, atIndex),
version: specifier.slice(atIndex + 1),
};
}
return { packageName: specifier };
}
function locateExtractedPackageDir(extractedRoot: string): string {
const packageDir = path.join(extractedRoot, 'package');
if (fs.existsSync(path.join(packageDir, 'matrix.json')) || fs.existsSync(path.join(packageDir, 'package.json'))) {
return packageDir;
}
if (fs.existsSync(path.join(extractedRoot, 'matrix.json')) || fs.existsSync(path.join(extractedRoot, 'package.json'))) {
return extractedRoot;
}
const entries = fs.readdirSync(extractedRoot, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const candidate = path.join(extractedRoot, entry.name);
if (fs.existsSync(path.join(candidate, 'matrix.json')) || fs.existsSync(path.join(candidate, 'package.json'))) {
return candidate;
}
}
throw new Error(`Unable to locate package manifest inside extracted tarball: ${extractedRoot}`);
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function readLifecycleHook(value: unknown, phase: LifecyclePhase): LifecycleHook | undefined {
const record = asRecord(value);
if (!record) {
return undefined;
}
const script = typeof record.script === 'string' ? record.script.trim() : '';
if (!script) {
throw new Error(`install.${phase}.script must be a non-empty string`);
}
const description = typeof record.description === 'string' ? record.description.trim() : '';
return description ? { script, description } : { script };
}
function readInstallLifecycle(manifest: JsonRecord): InstallLifecycle {
const install = asRecord(manifest.install);
if (!install) {
return {};
}
return {
validate: readLifecycleHook(install.validate, 'validate'),
migrate: readLifecycleHook(install.migrate, 'migrate'),
seed: readLifecycleHook(install.seed, 'seed'),
verify: readLifecycleHook(install.verify, 'verify'),
};
}
function readPackageJson(packageDir: string): JsonRecord | null {
const packageJsonPath = path.join(packageDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return null;
}
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as unknown;
return asRecord(parsed);
}
function hasDependencyEntries(value: unknown): boolean {
const record = asRecord(value);
return Boolean(record && Object.keys(record).length > 0);
}
function readRuntimeNpmDependencies(packageDir: string): RuntimeNpmDependencies | null {
const packageJson = readPackageJson(packageDir);
if (!packageJson) {
return null;
}
const dependencies = hasDependencyEntries(packageJson.dependencies)
? asRecord(packageJson.dependencies) ?? undefined
: undefined;
const optionalDependencies = hasDependencyEntries(packageJson.optionalDependencies)
? asRecord(packageJson.optionalDependencies) ?? undefined
: undefined;
if (!dependencies && !optionalDependencies) {
return null;
}
return {
...(dependencies ? { dependencies } : {}),
...(optionalDependencies ? { optionalDependencies } : {}),
};
}
function registryAuthConfigKey(registryUrl: string): string {
const parsed = new URL(registryUrl);
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
return `//${parsed.host}${pathname}:_authToken`;
}
export function dependencyInstallNpmrcLines(params: {
readonly registry?: string;
readonly token?: string;
}): readonly string[] {
const lines = [
'registry=https://registry.npmjs.org/',
];
if (params.registry) {
for (const scope of MATRIX_NPM_REGISTRY_SCOPES) {
lines.push(`${scope}:registry=${params.registry}`);
}
if (params.token) {
lines.push('always-auth=true');
lines.push(`${registryAuthConfigKey(params.registry)}=${params.token}`);
}
}
return lines;
}
function writeDependencyInstallNpmrc(params: {
readonly tempDir: string;
readonly registry?: string;
readonly token?: string;
}): string {
const npmrcPath = path.join(params.tempDir, '.npmrc');
const lines = dependencyInstallNpmrcLines(params);
fs.writeFileSync(npmrcPath, `${lines.join('\n')}\n`, 'utf8');
return npmrcPath;
}
function writeEmptyNpmGlobalConfig(tempDir: string): string {
const npmrcPath = path.join(tempDir, 'empty-global-npmrc');
fs.writeFileSync(npmrcPath, '', 'utf8');
return npmrcPath;
}
function runtimeDependencyPackageName(packageName: string): string {
const normalized = packageName.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+/, '');
return `mx-runtime-deps-${normalized || 'package'}`;
}
function installRuntimeNpmDependencies(params: {
readonly packageName: string;
readonly packageDir: string;
readonly dependencyOptions: DependencyInstallOptions;
readonly lifecyclePhases: string[];
}): void {
const runtimeDependencies = readRuntimeNpmDependencies(params.packageDir);
if (!params.dependencyOptions.install || !runtimeDependencies) {
return;
}
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-deps-'));
const packageJsonPath = path.join(params.packageDir, 'package.json');
const originalPackageJson = fs.readFileSync(packageJsonPath, 'utf8');
try {
fs.writeFileSync(
packageJsonPath,
`${JSON.stringify({
private: true,
name: runtimeDependencyPackageName(params.packageName),
version: '0.0.0',
...runtimeDependencies,
}, null, 2)}\n`,
'utf8',
);
const credential = params.dependencyOptions.registry
? readRegistryCredential(params.dependencyOptions.cwd, {
registry: params.dependencyOptions.registry,
credentialsPath: params.dependencyOptions.credentialsPath,
}).credential
: null;
const userConfig = writeDependencyInstallNpmrc({
tempDir,
registry: params.dependencyOptions.registry,
token: credential?.token,
});
const globalConfig = writeEmptyNpmGlobalConfig(tempDir);
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const result = spawnSync(npmCommand, [
'install',
'--omit=dev',
'--omit=peer',
'--no-audit',
'--fund=false',
'--package-lock=false',
'--userconfig',
userConfig,
'--globalconfig',
globalConfig,
], {
cwd: params.packageDir,
encoding: 'utf8',
env: {
...process.env,
npm_config_cache: path.join(tempDir, '.npm-cache'),
NPM_CONFIG_USERCONFIG: userConfig,
npm_config_userconfig: userConfig,
NPM_CONFIG_GLOBALCONFIG: globalConfig,
npm_config_globalconfig: globalConfig,
},
windowsHide: true,
});
if (result.status !== 0) {
const stderr = result.stderr?.trim();
const stdout = result.stdout?.trim();
const output = [stderr, stdout].filter(Boolean).join('\n');
throw new Error(
`${params.packageName} npm dependency install failed`
+ (output ? `\n${output}` : '')
);
}
params.lifecyclePhases.push('dependencies');
} finally {
fs.writeFileSync(packageJsonPath, originalPackageJson, 'utf8');
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
function runInstallLifecycle(params: {
lifecycle: InstallLifecycle;
packageName: string;
version: string;
packageDir: string;
packagesRoot: string;
installSource: string;
lifecyclePhases: string[];
}): void {
const phases: LifecyclePhase[] = ['validate', 'migrate', 'seed', 'verify'];
for (const phase of phases) {
const hook = params.lifecycle[phase];
if (!hook) continue;
const scriptPath = path.join(params.packageDir, hook.script);
if (!fs.existsSync(scriptPath)) {
throw new Error(`${params.packageName} install.${phase}.script does not exist (${hook.script})`);
}
const result = spawnSync(process.execPath, [scriptPath], {
cwd: params.packageDir,
encoding: 'utf8',
env: {
...process.env,
MATRIX_PACKAGES_DIR: params.packagesRoot,
MATRIX_PACKAGE_DIR: params.packageDir,
MATRIX_PACKAGE_NAME: params.packageName,
MATRIX_PACKAGE_VERSION: params.version,
MATRIX_INSTALL_SOURCE: params.installSource,
MATRIX_INSTALL_PHASE: phase,
},
windowsHide: true,
});
if (result.status !== 0) {
const stderr = result.stderr?.trim();
const stdout = result.stdout?.trim();
const output = [stderr, stdout].filter(Boolean).join('\n');
throw new Error(
`${params.packageName} install.${phase} failed`
+ (output ? `\n${output}` : '')
);
}
params.lifecyclePhases.push(phase);
}
}
function installFromDirectory(
sourceDir: string,
packagesRoot: string,
nodeModulesRoot: string,
force: boolean,
installSource: string,
dependencyOptions: DependencyInstallOptions,
): { packageName: string; version: string; targetDir: string; lifecyclePhases: string[] } {
const { packageName, manifest } = readManifestFromPackageDir(sourceDir);
validateResolvedManifest(packageName, manifest);
const version = readVersionFromPackageDir(sourceDir);
const lifecycle = readInstallLifecycle(manifest);
const targetDir = path.join(nodeModulesRoot, packageNameToPath(packageName));
const stageDir = `${targetDir}.installing-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const backupDir = `${targetDir}.backup-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const hadExistingTarget = fs.existsSync(targetDir);
if (hadExistingTarget && !force) {
throw new Error(`Package already installed: ${packageName} (${targetDir})`);
}
fs.mkdirSync(path.dirname(stageDir), { recursive: true });
fs.cpSync(sourceDir, stageDir, { recursive: true });
const lifecyclePhases: string[] = [];
try {
if (hadExistingTarget) {
fs.renameSync(targetDir, backupDir);
}
fs.renameSync(stageDir, targetDir);
installRuntimeNpmDependencies({
packageName,
packageDir: targetDir,
dependencyOptions,
lifecyclePhases,
});
runInstallLifecycle({
lifecycle,
packageName,
version,
packageDir: targetDir,
packagesRoot,
installSource,
lifecyclePhases,
});
if (hadExistingTarget && fs.existsSync(backupDir)) {
fs.rmSync(backupDir, { recursive: true, force: true });
}
} catch (error) {
if (fs.existsSync(stageDir)) {
fs.rmSync(stageDir, { recursive: true, force: true });
}
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true });
}
if (hadExistingTarget && fs.existsSync(backupDir)) {
fs.renameSync(backupDir, targetDir);
}
throw error;
}
return {
packageName,
version,
targetDir,
lifecyclePhases,
};
}
function runtimeEntryFromManifest(manifest: JsonRecord): string | null {
const runtime = asRecord(manifest.runtime);
const entry = typeof runtime?.entry === 'string' ? runtime.entry.trim() : '';
return entry.length > 0 ? entry : null;
}
function hasWebappSection(manifest: JsonRecord): boolean {
return asRecord(manifest.webapp) !== null;
}
function readWebappConfig(manifest: JsonRecord): { distDir: string; entryFile: string } | null {
const webapp = asRecord(manifest.webapp);
if (!webapp) return null;
const distDir = typeof webapp.distDir === 'string' && webapp.distDir.trim().length > 0
? webapp.distDir.trim()
: 'dist';
const entryFile = typeof webapp.entry === 'string' && webapp.entry.trim().length > 0
? webapp.entry.trim()
: 'index.html';
return { distDir, entryFile };
}
function walkFiles(rootDir: string): string[] {
if (!fs.existsSync(rootDir)) {
return [];
}
const results: string[] = [];
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop()!;
const entries = fs.readdirSync(current, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile()) {
results.push(fullPath);
}
}
}
return results;
}
async function maybeBuildLocalTypescriptPackage(sourceDir: string): Promise<string> {
const { manifest } = readManifestFromPackageDir(sourceDir);
const runtimeEntry = runtimeEntryFromManifest(manifest);
const webapp = readWebappConfig(manifest);
if (!runtimeEntry && !webapp) {
return sourceDir;
}
const sourceEntry = runtimeEntry ? path.join(sourceDir, runtimeEntry) : null;
const webappEntryPath = webapp ? path.join(sourceDir, webapp.distDir, webapp.entryFile) : null;
if ((sourceEntry && fs.existsSync(sourceEntry)) && (!webappEntryPath || fs.existsSync(webappEntryPath))) {
return sourceDir;
}
const srcDir = path.join(sourceDir, 'src');
if (!fs.existsSync(srcDir)) {
return sourceDir;
}
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-build-'));
fs.cpSync(sourceDir, stagingDir, { recursive: true });
const stagedSrcDir = path.join(stagingDir, 'src');
const stagedRuntimeEntry = runtimeEntry ? path.join(stagingDir, runtimeEntry) : null;
const stagedDistDir = stagedRuntimeEntry
? path.dirname(stagedRuntimeEntry)
: path.join(stagingDir, webapp?.distDir ?? 'dist');
fs.mkdirSync(stagedDistDir, { recursive: true });
const ts = await import('typescript');
for (const filePath of walkFiles(stagedSrcDir)) {
const relativePath = path.relative(stagedSrcDir, filePath);
const extension = path.extname(filePath).toLowerCase();
const outputPath = path.join(
stagedDistDir,
relativePath.replace(/\.(mts|cts|tsx|ts)$/i, '.js'),
);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
if (extension === '.ts' || extension === '.tsx' || extension === '.mts' || extension === '.cts') {
const source = fs.readFileSync(filePath, 'utf8');
const transpiled = ts.transpileModule(source, {
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeNext,
esModuleInterop: true,
sourceMap: false,
inlineSourceMap: false,
declaration: false,
},
fileName: filePath,
});
fs.writeFileSync(outputPath, transpiled.outputText, 'utf8');
continue;
}
fs.copyFileSync(filePath, path.join(stagedDistDir, relativePath));
}
if (webapp) {
const sourceHtmlPath = path.join(stagingDir, webapp.entryFile);
const stagedHtmlPath = path.join(stagingDir, webapp.distDir, webapp.entryFile);
if (fs.existsSync(sourceHtmlPath) && !fs.existsSync(stagedHtmlPath)) {
let html = fs.readFileSync(sourceHtmlPath, 'utf8');
html = html.replace(
/(<script[^>]+src=["'])\.\/src\/([^"']+)\.(?:ts|tsx|mts|cts)(["'][^>]*>)/g,
'$1./$2.js$3',
);
fs.mkdirSync(path.dirname(stagedHtmlPath), { recursive: true });
fs.writeFileSync(stagedHtmlPath, html, 'utf8');
}
}
return stagingDir;
}
export async function installCommand(packagePath: string, options: IInstallCommandOptions = {}): Promise<IInstallCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const packagesRoot = path.resolve(cwd, options.packagesDir ?? path.join(os.homedir(), '.matrix', 'packages'));
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
fs.mkdirSync(nodeModulesRoot, { recursive: true });
const normalizedPackagePath = packagePath.trim();
if (!normalizedPackagePath) {
throw new Error('Install source is required');
}
const resolvedPath = path.resolve(cwd, normalizedPackagePath);
const force = Boolean(options.force);
if (fs.existsSync(resolvedPath)) {
if (fs.statSync(resolvedPath).isDirectory()) {
const preparedSourceDir = await maybeBuildLocalTypescriptPackage(resolvedPath);
try {
const installed = installFromDirectory(preparedSourceDir, packagesRoot, nodeModulesRoot, force, resolvedPath, {
install: false,
cwd,
});
return {
packageName: installed.packageName,
source: resolvedPath,
version: installed.version,
targetDir: installed.targetDir,
lifecyclePhases: installed.lifecyclePhases,
};
} finally {
if (preparedSourceDir !== resolvedPath) {
fs.rmSync(preparedSourceDir, { recursive: true, force: true });
}
}
}
if (resolvedPath.endsWith('.tar.gz') || resolvedPath.endsWith('.tgz')) {
const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-tarball-'));
try {
extractTarball(resolvedPath, extractRoot);
const sourceDir = locateExtractedPackageDir(extractRoot);
const installed = installFromDirectory(sourceDir, packagesRoot, nodeModulesRoot, force, resolvedPath, {
install: true,
registry: resolvePackageRegistry(options.registry, { cwd }).registry,
credentialsPath: options.credentialsPath,
cwd,
});
return {
packageName: installed.packageName,
source: resolvedPath,
version: installed.version,
targetDir: installed.targetDir,
lifecyclePhases: installed.lifecyclePhases,
};
} finally {
fs.rmSync(extractRoot, { recursive: true, force: true });
}
}
throw new Error(`Unsupported install source: ${resolvedPath}`);
}
const { packageName, version } = parsePackageSpecifier(normalizedPackagePath);
if (options.registry && options.registryDir) {
throw new Error('MX_REGISTRY_INVALID: use either --registry or --registry-dir, not both');
}
if (!options.registryDir || options.registry) {
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
const specifier = npmPackageSpecifier(packageName, version);
const fetched = await prepareNpmRegistryTarball(
specifier,
registry,
cwd,
options.credentialsPath,
);
const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-npm-extract-'));
try {
extractTarball(fetched.tarballPath, extractRoot);
const sourceDir = locateExtractedPackageDir(extractRoot);
const installed = installFromDirectory(sourceDir, packagesRoot, nodeModulesRoot, force, specifier, {
install: true,
registry,
credentialsPath: options.credentialsPath,
cwd,
});
return {
packageName: installed.packageName,
source: `${installed.packageName}@${installed.version}`,
version: installed.version,
targetDir: installed.targetDir,
lifecyclePhases: installed.lifecyclePhases,
};
} finally {
fs.rmSync(extractRoot, { recursive: true, force: true });
fs.rmSync(fetched.cleanupDir, { recursive: true, force: true });
}
}
const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
const resolved = resolveRegistryTarball(registryRoot, packageName, version);
const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-registry-'));
try {
extractTarball(resolved.tarballPath, extractRoot);
const sourceDir = locateExtractedPackageDir(extractRoot);
const installed = installFromDirectory(sourceDir, packagesRoot, nodeModulesRoot, force, `${packageName}@${resolved.version}`, {
install: true,
registry: resolvePackageRegistry(undefined, { cwd }).registry,
cwd,
});
return {
packageName: installed.packageName,
source: `${packageName}@${resolved.version}`,
version: installed.version,
targetDir: installed.targetDir,
lifecyclePhases: installed.lifecyclePhases,
};
} finally {
fs.rmSync(extractRoot, { recursive: true, force: true });
}
}

View File

@ -0,0 +1,19 @@
import * as path from 'node:path';
import { listInstalledPackageNames, resolvePackagesNodeModulesRoot } from '../utils/package-store.js';
export interface IListCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
}
export interface IListCommandResult {
readonly packages: string[];
}
export async function listCommand(options: IListCommandOptions = {}): Promise<IListCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
return {
packages: listInstalledPackageNames(nodeModulesRoot),
};
}

View File

@ -0,0 +1,91 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
listInstalledPackageNames,
packageNameToPath,
resolvePackagesNodeModulesRoot,
resolveRegistryRoot,
} from '../utils/package-store.js';
import { getLatestRegistryVersion } from '../utils/registry-store.js';
import { compareSemver } from '../utils/versioning.js';
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function readForkRecord(forkRecordPath: string): {
readonly origin: string;
readonly originVersion: string;
} | null {
if (!fs.existsSync(forkRecordPath)) {
return null;
}
const parsed = JSON.parse(fs.readFileSync(forkRecordPath, 'utf8')) as unknown;
const record = asRecord(parsed);
const origin = typeof record?.origin === 'string' ? record.origin.trim() : '';
const originVersion = typeof record?.originVersion === 'string' ? record.originVersion.trim() : '';
if (!origin || !originVersion) {
return null;
}
return { origin, originVersion };
}
export interface IOutdatedCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
readonly registryDir?: string;
}
export interface IOutdatedEntry {
readonly packageName: string;
readonly origin: string;
readonly localOriginVersion: string;
readonly latestOriginVersion: string | null;
readonly status: 'up-to-date' | 'outdated' | 'missing-upstream';
}
export interface IOutdatedCommandResult {
readonly entries: ReadonlyArray<IOutdatedEntry>;
}
export async function outdatedCommand(options: IOutdatedCommandOptions = {}): Promise<IOutdatedCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
const entries: IOutdatedEntry[] = [];
for (const packageName of listInstalledPackageNames(nodeModulesRoot)) {
const packageDir = path.join(nodeModulesRoot, packageNameToPath(packageName));
const forkRecord = readForkRecord(path.join(packageDir, '.forked-from'));
if (!forkRecord) {
continue;
}
const latestOriginVersion = getLatestRegistryVersion(registryRoot, forkRecord.origin);
if (!latestOriginVersion) {
entries.push({
packageName,
origin: forkRecord.origin,
localOriginVersion: forkRecord.originVersion,
latestOriginVersion: null,
status: 'missing-upstream',
});
continue;
}
entries.push({
packageName,
origin: forkRecord.origin,
localOriginVersion: forkRecord.originVersion,
latestOriginVersion,
status: compareSemver(latestOriginVersion, forkRecord.originVersion) > 0 ? 'outdated' : 'up-to-date',
});
}
return { entries };
}

View File

@ -0,0 +1,65 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { createTarball } from '../utils/archive.js';
import {
readManifestFromPackageDir,
readVersionFromPackageDir,
validateResolvedManifest,
} from '../utils/package-store.js';
export interface IPackCommandOptions {
readonly cwd?: string;
readonly outDir?: string;
readonly buildFirst?: boolean;
}
export interface IPackCommandResult {
readonly packageName: string;
readonly version: string;
readonly tarballPath: string;
readonly sourceDir: string;
}
function packageFileName(packageName: string, version: string): string {
const normalized = packageName.replace(/^@/, '').split('/').join('-');
return `${normalized}-${version}.tar.gz`;
}
function runBuild(sourceDir: string): void {
const result = spawnSync('npm', ['run', 'build'], {
cwd: sourceDir,
encoding: 'utf8',
windowsHide: true,
});
if (result.status !== 0) {
throw new Error(`Build failed: ${result.stderr || result.stdout || `exit ${result.status ?? 'unknown'}`}`);
}
}
export async function packCommand(options: IPackCommandOptions = {}): Promise<IPackCommandResult> {
const sourceDir = path.resolve(options.cwd ?? process.cwd());
if (!fs.existsSync(sourceDir)) {
throw new Error(`Package directory does not exist: ${sourceDir}`);
}
const { packageName, manifest } = readManifestFromPackageDir(sourceDir);
validateResolvedManifest(packageName, manifest);
const version = readVersionFromPackageDir(sourceDir);
if (options.buildFirst) {
runBuild(sourceDir);
}
const outDir = path.resolve(sourceDir, options.outDir ?? '.mx-pack');
fs.mkdirSync(outDir, { recursive: true });
const tarballPath = path.join(outDir, packageFileName(packageName, version));
createTarball(sourceDir, tarballPath);
return {
packageName,
version,
tarballPath,
sourceDir,
};
}

View File

@ -0,0 +1,173 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { packCommand } from './pack.js';
import { readRegistryCredential } from '../utils/auth-store.js';
import {
readManifestFromPackageDir,
readVersionFromPackageDir,
resolveRegistryRoot,
validateResolvedManifest,
} from '../utils/package-store.js';
import { publishRegistryVersion } from '../utils/registry-store.js';
import {
packNpmPublishTarball,
publishNpmTarball,
} from '../utils/npm-registry-client.js';
import { resolvePackageRegistry } from '../utils/config-store.js';
import { loadMatrixServiceManifest } from '../utils/service-manifest.js';
import { extractDiscoveryMetadata, type IPackageDiscoveryMetadata } from '../utils/discovery-extractor.js';
import { publishPackageDiscoveryMetadata } from '../utils/discovery-index.js';
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
export interface IPublishCommandOptions {
readonly cwd?: string;
readonly registryDir?: string;
readonly registry?: string;
readonly credentialsPath?: string;
readonly dryRun?: boolean;
}
export interface IPublishCommandResult {
readonly packageName: string;
readonly version: string;
readonly tarballPath: string;
readonly registryTarballPath?: string;
readonly registryUrl?: string;
readonly registryRoot: string;
readonly discoveryMetadata?: IPackageDiscoveryMetadata;
readonly dryRun: boolean;
}
function ensurePublishPreflight(packageDir: string): { packageName: string; manifest: JsonRecord } {
const { packageName, manifest } = readManifestFromPackageDir(packageDir);
validateResolvedManifest(packageName, manifest);
const runtime = asRecord(manifest.runtime);
const runtimeEntry = typeof runtime?.entry === 'string' ? runtime.entry.trim() : '';
if (!runtimeEntry) {
throw new Error('MX_MANIFEST_INVALID: runtime.entry is required');
}
if (!fs.existsSync(path.join(packageDir, runtimeEntry))) {
throw new Error(`MX_MANIFEST_INVALID: runtime.entry does not exist (${runtimeEntry})`);
}
const hasComponents = Array.isArray(manifest.components) && manifest.components.length > 0;
const root = asRecord(manifest.root);
const hasRoot = typeof root?.type === 'string'
&& root.type.trim().length > 0
&& typeof root?.export === 'string'
&& root.export.trim().length > 0;
const hasServiceFactory = asRecord(runtime?.factory) !== null;
if (hasServiceFactory) {
loadMatrixServiceManifest(packageDir);
}
const webapp = asRecord(manifest.webapp);
const webappDistDir = typeof webapp?.distDir === 'string' ? webapp.distDir.trim() : '';
const webappEntry = typeof webapp?.entry === 'string' ? webapp.entry.trim() : '';
const hasWebapp = Boolean(webapp && webappDistDir && webappEntry);
if (hasWebapp && !fs.existsSync(path.join(packageDir, webappDistDir, webappEntry))) {
throw new Error(`MX_MANIFEST_INVALID: webapp entry does not exist (${path.join(webappDistDir, webappEntry)})`);
}
if (!hasComponents && !hasWebapp && !hasRoot && !hasServiceFactory) {
throw new Error('MX_MANIFEST_INVALID: declare a root actor, component, webapp, or matrix.json runtime.factory');
}
return { packageName, manifest };
}
export async function publishCommand(options: IPublishCommandOptions = {}): Promise<IPublishCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
if (options.registry && options.registryDir) {
throw new Error('MX_REGISTRY_INVALID: use either --registry or --registry-dir, not both');
}
const registryUrl = options.registryDir && !options.registry
? undefined
: resolvePackageRegistry(options.registry, { cwd }).registry;
const auth = readRegistryCredential(cwd, {
...(registryUrl ? { registry: registryUrl } : {}),
credentialsPath: options.credentialsPath,
});
if (!auth.credential) {
throw new Error('MX_AUTH_REQUIRED: run `mx login` before publish');
}
const { packageName } = ensurePublishPreflight(cwd);
const version = readVersionFromPackageDir(cwd);
const discoveryMetadata = extractDiscoveryMetadata(cwd);
const packed = registryUrl
? await packNpmPublishTarball(cwd, path.join(cwd, '.mx-pack', 'npm'))
: await packCommand({
cwd,
outDir: '.mx-pack',
});
if (options.dryRun) {
return {
packageName,
version,
tarballPath: packed.tarballPath,
...(registryUrl ? { registryUrl } : {}),
registryRoot,
discoveryMetadata,
dryRun: true,
};
}
if (registryUrl) {
const result = await publishNpmTarball(
packed.tarballPath,
registryUrl,
cwd,
options.credentialsPath,
);
if (result.exitCode !== 0) {
const output = [result.stderr, result.stdout]
.filter((value) => typeof value === 'string' && value.trim().length > 0)
.join('\n')
.trim();
throw new Error(
`MX_REGISTRY_PUBLISH_FAILED: failed to publish ${packageName}@${version} to ${registryUrl}`
+ (output ? `\n${output}` : ''),
);
}
return {
packageName,
version,
tarballPath: packed.tarballPath,
registryUrl,
registryRoot,
discoveryMetadata,
dryRun: false,
};
}
const published = publishRegistryVersion(
registryRoot,
packageName,
version,
packed.tarballPath,
auth.credential.username,
);
publishPackageDiscoveryMetadata(registryRoot, discoveryMetadata, auth.credential.username);
return {
packageName,
version,
tarballPath: packed.tarballPath,
registryTarballPath: published.registryTarballPath,
registryRoot,
discoveryMetadata,
dryRun: false,
};
}

View File

@ -0,0 +1,312 @@
import { spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { loginCommand } from './auth.js';
import { configSetCommand } from './config.js';
import { readRegistryCredential } from '../utils/auth-store.js';
import { resolvePackageRegistry } from '../utils/config-store.js';
import { MATRIX_NPM_REGISTRY_SCOPES } from '../utils/npm-registry-client.js';
export interface IRegistryCommandOptions {
readonly cwd?: string;
readonly registry?: string;
readonly username?: string;
readonly token?: string;
readonly email?: string;
readonly expiresAt?: string;
readonly credentialsPath?: string;
readonly configPath?: string;
readonly timeoutMs?: number;
}
export interface IRegistryUseResult {
readonly ok: true;
readonly profile: string;
readonly registry: string;
readonly configPath: string;
}
export interface IRegistryLoginResult {
readonly ok: true;
readonly registry: string;
readonly username: string;
readonly credentialsPath: string;
}
export interface IRegistryDoctorResult {
readonly ok: boolean;
readonly mode: 'registry-doctor';
readonly registry: {
readonly url: string;
readonly source: string;
readonly reachable: boolean;
readonly status?: number;
readonly error?: string;
};
readonly credentials: {
readonly found: boolean;
readonly path: string;
readonly username?: string;
readonly authChecked: boolean;
readonly authOk?: boolean;
readonly whoami?: string;
readonly error?: string;
};
readonly npm: {
readonly userConfig: string;
readonly scopeResolution: Record<string, string>;
readonly allScopesMatch: boolean;
readonly poisonedUserConfigIgnored: boolean;
};
}
const LOCAL_GITEA_REGISTRY = 'http://127.0.0.1:3000/api/packages/open-matrix/npm/';
export async function registryUseCommand(
profile: string,
options: IRegistryCommandOptions = {},
): Promise<IRegistryUseResult> {
const cwd = options.cwd ?? process.cwd();
const registry = registryForProfile(profile, options.registry, cwd, options.configPath);
const result = await configSetCommand('registry', registry, {
cwd,
configPath: options.configPath,
});
return {
ok: true,
profile,
registry: result.value,
configPath: result.configPath,
};
}
export async function registryLoginCommand(options: IRegistryCommandOptions = {}): Promise<IRegistryLoginResult> {
if (!options.username || !options.token) {
throw new Error('MX_REGISTRY_LOGIN_REQUIRED: --username and --token are required');
}
const result = await loginCommand({
username: options.username,
token: options.token,
email: options.email,
expiresAt: options.expiresAt,
registry: options.registry,
credentialsPath: options.credentialsPath,
cwd: options.cwd,
});
return {
ok: true,
registry: result.registry,
username: result.username,
credentialsPath: result.credentialsPath,
};
}
export async function registryDoctorCommand(options: IRegistryCommandOptions = {}): Promise<IRegistryDoctorResult> {
const cwd = options.cwd ?? process.cwd();
const resolved = resolvePackageRegistry(options.registry, {
cwd,
configPath: options.configPath,
});
const credential = readRegistryCredential(cwd, {
registry: resolved.registry,
credentialsPath: options.credentialsPath,
});
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-registry-doctor-'));
try {
const userConfig = writeDoctorNpmrc(tempDir, resolved.registry, credential.credential?.token ?? null);
const npmRegistry = await runNpm(['config', 'get', 'registry', '--userconfig', userConfig], {
cwd,
registryUrl: resolved.registry,
userConfig,
timeoutMs: options.timeoutMs,
});
const scopeResolution: Record<string, string> = {};
for (const scope of MATRIX_NPM_REGISTRY_SCOPES) {
const result = await runNpm(['config', 'get', `${scope}:registry`, '--userconfig', userConfig], {
cwd,
registryUrl: resolved.registry,
userConfig,
timeoutMs: options.timeoutMs,
});
scopeResolution[scope] = result.stdout.trim();
}
const registryValue = npmRegistry.stdout.trim();
const allScopesMatch = registryValue === resolved.registry
&& MATRIX_NPM_REGISTRY_SCOPES.every((scope) => scopeResolution[scope] === resolved.registry);
const reachability = await checkRegistryReachability(resolved.registry, options.timeoutMs ?? 3_000);
const auth = credential.credential
? await checkNpmWhoami(cwd, resolved.registry, userConfig, options.timeoutMs)
: { checked: false as const };
const ok = allScopesMatch
&& (auth.checked ? auth.ok === true : true);
return {
ok,
mode: 'registry-doctor',
registry: {
url: resolved.registry,
source: resolved.source,
reachable: reachability.reachable,
...(reachability.status !== undefined ? { status: reachability.status } : {}),
...(reachability.error ? { error: reachability.error } : {}),
},
credentials: {
found: Boolean(credential.credential),
path: credential.credentialsPath,
...(credential.credential?.username ? { username: credential.credential.username } : {}),
authChecked: auth.checked,
...(auth.checked ? { authOk: auth.ok } : {}),
...(auth.checked && auth.whoami ? { whoami: auth.whoami } : {}),
...(auth.checked && auth.error ? { error: auth.error } : {}),
},
npm: {
userConfig,
scopeResolution,
allScopesMatch,
poisonedUserConfigIgnored: allScopesMatch,
},
};
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
function registryForProfile(
profile: string,
explicitRegistry: string | undefined,
cwd: string,
configPath: string | undefined,
): string {
if (profile === 'local-gitea') {
return resolvePackageRegistry(explicitRegistry ?? LOCAL_GITEA_REGISTRY, { cwd, configPath }).registry;
}
if (explicitRegistry) {
return resolvePackageRegistry(explicitRegistry, { cwd, configPath }).registry;
}
if (profile.startsWith('http://') || profile.startsWith('https://')) {
return resolvePackageRegistry(profile, { cwd, configPath }).registry;
}
throw new Error(`MX_REGISTRY_PROFILE_UNKNOWN: ${profile}`);
}
function writeDoctorNpmrc(tempDir: string, registry: string, token: string | null): string {
const filePath = path.join(tempDir, '.npmrc');
const lines = [
`registry=${registry}`,
...MATRIX_NPM_REGISTRY_SCOPES.map((scope) => `${scope}:registry=${registry}`),
];
if (token) {
lines.push('always-auth=true');
lines.push(`${registryAuthConfigKey(registry)}=${token}`);
}
fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
return filePath;
}
function registryAuthConfigKey(registry: string): string {
const parsed = new URL(registry);
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
return `//${parsed.host}${pathname}:_authToken`;
}
async function checkRegistryReachability(
registry: string,
timeoutMs: number,
): Promise<{ readonly reachable: boolean; readonly status?: number; readonly error?: string }> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(registry, {
method: 'GET',
signal: controller.signal,
headers: { accept: 'application/json,text/plain,*/*' },
});
return { reachable: true, status: response.status };
} catch (error) {
return {
reachable: false,
error: error instanceof Error ? error.message : String(error),
};
} finally {
clearTimeout(timeout);
}
}
async function checkNpmWhoami(
cwd: string,
registry: string,
userConfig: string,
timeoutMs: number | undefined,
): Promise<{
readonly checked: true;
readonly ok: boolean;
readonly whoami?: string;
readonly error?: string;
}> {
const result = await runNpm(['whoami', '--registry', registry, '--userconfig', userConfig], {
cwd,
registryUrl: registry,
userConfig,
timeoutMs,
});
const output = [result.stderr, result.stdout]
.map((value) => value.trim())
.filter(Boolean)
.join('\n');
return {
checked: true,
ok: result.exitCode === 0,
...(result.exitCode === 0 && result.stdout.trim() ? { whoami: result.stdout.trim() } : {}),
...(result.exitCode === 0 ? {} : { error: output || `npm whoami exited ${result.exitCode}` }),
};
}
async function runNpm(
args: readonly string[],
options: {
readonly cwd: string;
readonly registryUrl: string;
readonly userConfig: string;
readonly timeoutMs?: number;
},
): Promise<{ readonly exitCode: number | null; readonly stdout: string; readonly stderr: string }> {
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
return await new Promise((resolve, reject) => {
const child = spawn(npmCommand, args, {
cwd: options.cwd,
env: {
...process.env,
npm_config_userconfig: options.userConfig,
NPM_CONFIG_USERCONFIG: options.userConfig,
npm_config_registry: options.registryUrl,
NPM_CONFIG_REGISTRY: options.registryUrl,
npm_config_cache: path.join(path.dirname(options.userConfig), '.npm-cache'),
},
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
const stdout: string[] = [];
const stderr: string[] = [];
const timeout = setTimeout(() => {
child.kill('SIGTERM');
}, options.timeoutMs ?? 10_000);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => stdout.push(chunk));
child.stderr.on('data', (chunk: string) => stderr.push(chunk));
child.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
child.on('close', (exitCode) => {
clearTimeout(timeout);
resolve({
exitCode,
stdout: stdout.join(''),
stderr: stderr.join(''),
});
});
});
}

View File

@ -0,0 +1,89 @@
import { resolvePackageRegistry } from '../utils/config-store.js';
import { resolveRegistryRoot } from '../utils/package-store.js';
import { searchPackageDiscoveryIndex } from '../utils/discovery-index.js';
import {
searchNpmRegistry,
type INpmRegistrySearchResult,
} from '../utils/npm-registry-client.js';
export interface ISearchCommandOptions {
readonly cwd?: string;
readonly registryDir?: string;
readonly registry?: string;
readonly credentialsPath?: string;
readonly limit?: number;
}
export interface IPackageSearchResult extends INpmRegistrySearchResult {
readonly displayName?: string;
readonly inferredKind?: string;
readonly declaredKind?: string;
readonly matchedFields?: readonly string[];
readonly score?: number;
}
export interface ISearchCommandResult {
readonly query: string;
readonly registry: string;
readonly registrySource: 'explicit' | 'env' | 'config' | 'default' | 'local';
readonly results: readonly IPackageSearchResult[];
}
export async function searchCommand(
query: string,
options: ISearchCommandOptions = {},
): Promise<ISearchCommandResult> {
const cwd = options.cwd ?? process.cwd();
const limit = normalizeLimit(options.limit);
if (options.registry && options.registryDir) {
throw new Error('MX_REGISTRY_INVALID: use either --registry or --registry-dir, not both');
}
if (options.registryDir) {
const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
const results = searchPackageDiscoveryIndex(
registryRoot,
{ text: query.trim() },
limit,
).map((entry): IPackageSearchResult => ({
packageName: entry.packageName,
version: entry.version,
...(entry.description ? { description: entry.description } : {}),
...(entry.displayName ? { displayName: entry.displayName } : {}),
...(entry.inferredKind ? { inferredKind: entry.inferredKind } : {}),
...(entry.declaredKind ? { declaredKind: entry.declaredKind } : {}),
keywords: entry.keywords,
matchedFields: entry.matchedFields,
score: entry.score,
}));
return {
query: query.trim(),
registry: registryRoot,
registrySource: 'local',
results,
};
}
const registry = resolvePackageRegistry(options.registry, { cwd });
const results = await searchNpmRegistry(
query,
registry.registry,
cwd,
options.credentialsPath,
limit,
);
return {
query: query.trim(),
registry: registry.registry,
registrySource: registry.source,
results,
};
}
function normalizeLimit(value: number | undefined): number {
if (value === undefined) {
return 20;
}
if (!Number.isInteger(value) || value <= 0 || value > 100) {
throw new Error('MX_SEARCH_INVALID: --limit must be an integer from 1 to 100');
}
return value;
}

View File

@ -0,0 +1,26 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { packageNameToPath, resolvePackagesNodeModulesRoot } from '../utils/package-store.js';
export interface IUninstallCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
}
export interface IUninstallCommandResult {
readonly packageName: string;
readonly removedPath: string;
}
export async function uninstallCommand(packageName: string, options: IUninstallCommandOptions = {}): Promise<IUninstallCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
const removedPath = path.join(nodeModulesRoot, packageNameToPath(packageName));
if (!fs.existsSync(removedPath)) {
throw new Error(`Package is not installed: ${packageName}`);
}
fs.rmSync(removedPath, { recursive: true, force: true });
return { packageName, removedPath };
}

View File

@ -0,0 +1,95 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
packageNameToPath,
resolvePackagesNodeModulesRoot,
resolveRegistryRoot,
} from '../utils/package-store.js';
import { resolveRegistryTarball } from '../utils/registry-store.js';
import { compareSemver } from '../utils/versioning.js';
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
interface IForkRecord {
origin: string;
originVersion: string;
[key: string]: unknown;
}
function readForkRecord(filePath: string): IForkRecord {
if (!fs.existsSync(filePath)) {
throw new Error(`Fork metadata missing: ${filePath}`);
}
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
const record = asRecord(parsed);
const origin = typeof record?.origin === 'string' ? record.origin.trim() : '';
const originVersion = typeof record?.originVersion === 'string' ? record.originVersion.trim() : '';
if (!origin || !originVersion) {
throw new Error(`Invalid fork metadata in ${filePath}`);
}
return {
...(record ?? {}),
origin,
originVersion,
};
}
function writeForkRecord(filePath: string, payload: IForkRecord): void {
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
}
export interface IUpdateForkCommandOptions {
readonly cwd?: string;
readonly packagesDir?: string;
readonly registryDir?: string;
readonly version?: string;
}
export interface IUpdateForkCommandResult {
readonly packageName: string;
readonly origin: string;
readonly previousOriginVersion: string;
readonly nextOriginVersion: string;
readonly updated: boolean;
}
export async function updateForkCommand(
packageName: string,
options: IUpdateForkCommandOptions = {},
): Promise<IUpdateForkCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const nodeModulesRoot = resolvePackagesNodeModulesRoot(cwd, options.packagesDir);
const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
const packageDir = path.join(nodeModulesRoot, packageNameToPath(packageName));
if (!fs.existsSync(packageDir)) {
throw new Error(`Package is not installed: ${packageName}`);
}
const forkRecordPath = path.join(packageDir, '.forked-from');
const forkRecord = readForkRecord(forkRecordPath);
const upstream = resolveRegistryTarball(registryRoot, forkRecord.origin, options.version);
const previousOriginVersion = forkRecord.originVersion;
const nextOriginVersion = upstream.version;
const updated = compareSemver(nextOriginVersion, previousOriginVersion) > 0;
if (updated) {
forkRecord.originVersion = nextOriginVersion;
forkRecord.updatedAt = new Date().toISOString();
writeForkRecord(forkRecordPath, forkRecord);
}
return {
packageName,
origin: forkRecord.origin,
previousOriginVersion,
nextOriginVersion,
updated,
};
}

View File

@ -0,0 +1,94 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { validateManifestForMxCli } from '../utils/manifestValidator.js';
import { readManifestFromPackageDir } from '../utils/package-store.js';
type JsonRecord = Record<string, unknown>;
export interface IValidateCommandOptions {
readonly cwd?: string;
}
export interface IValidateCommandResult {
readonly packageName: string;
readonly valid: boolean;
readonly diagnostics: ReadonlyArray<{ code: string; message: string; field?: string; componentIndex?: number }>;
}
function readJsonObject(filePath: string): JsonRecord {
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${filePath} must contain a JSON object`);
}
return parsed as JsonRecord;
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function resolveManifest(validationTarget: string): { packageName: string; manifest: JsonRecord } {
const targetStat = fs.statSync(validationTarget);
if (targetStat.isDirectory()) {
return readManifestFromPackageDir(validationTarget);
}
const normalized = path.basename(validationTarget).toLowerCase();
if (normalized === 'matrix.json') {
const manifest = readJsonObject(validationTarget);
const packageName = typeof manifest.name === 'string' ? manifest.name.trim() : '';
if (!packageName) {
throw new Error(`matrix.json must contain a non-empty "name" field (${validationTarget})`);
}
return { packageName, manifest };
}
if (normalized === 'package.json') {
const pkg = readJsonObject(validationTarget);
const packageName = typeof pkg.name === 'string' ? pkg.name.trim() : '';
if (!packageName) {
throw new Error(`package.json must contain a non-empty "name" field (${validationTarget})`);
}
const matrix = asRecord(pkg.matrix) ?? {};
return {
packageName,
manifest: {
name: packageName,
version: pkg.version,
runtime: matrix.runtime,
components: matrix.components,
permissions: matrix.permissions,
},
};
}
throw new Error('Validation target must be a directory, matrix.json, or package.json');
}
export async function validateCommand(
target: string | undefined,
options: IValidateCommandOptions = {}
): Promise<IValidateCommandResult> {
const cwd = path.resolve(options.cwd ?? process.cwd());
const resolvedTarget = path.resolve(cwd, target ?? '.');
if (!fs.existsSync(resolvedTarget)) {
throw new Error(`Validation target does not exist: ${resolvedTarget}`);
}
const { packageName, manifest } = resolveManifest(resolvedTarget);
const result = validateManifestForMxCli(manifest);
return {
packageName,
valid: result.valid,
diagnostics: result.diagnostics.map((entry) => ({
code: entry.code,
message: entry.message,
field: entry.field,
componentIndex: entry.componentIndex,
})),
};
}

View File

@ -0,0 +1,553 @@
/**
* mx webapp command implementation.
*
* Scaffolds a complete browser web app package with:
* - Domain actor (MatrixActor) for business logic
* - Shell (MatrixActorHtmlElement) for browser UI
* - Federation bootstrap (init.ts)
* - Vite build config
* - HTML template + CSS styles
* - Tests and examples
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
toKebabCase, toPascalCase, validateMatrixManifest, writeJsonFile,
generatePackageJson, generateTsconfig,
} from '../utils/scaffold.js';
export interface IWebappCommandOptions {
readonly cwd?: string;
readonly outputDir?: string;
}
export interface IWebappCommandResult {
readonly rootDir: string;
readonly appClassName: string;
readonly shellClassName: string;
readonly tagName: string;
readonly appName: string;
}
// ── Template generators ─────────────────────────────────────────────────
function appActorTemplate(className: string): string {
return `import { MatrixActor } from '@open-matrix/core';
export class ${className} extends MatrixActor {
static override accepts = {
increment: {},
decrement: {},
getCount: {},
reset: {},
};
static override emits = {
countChanged: { count: 'number' },
};
private _count = 0;
protected onIncrement(): void {
this._count++;
this.emit('countChanged', { count: this._count });
}
protected onDecrement(): void {
this._count--;
this.emit('countChanged', { count: this._count });
}
protected onGetCount(): { count: number } {
return { count: this._count };
}
protected onReset(): void {
this._count = 0;
this.emit('countChanged', { count: this._count });
}
}
`;
}
function shellTemplate(shellClassName: string, appClassName: string, tagName: string): string {
return `import { MatrixActorHtmlElement } from '@open-matrix/core';
import { ${appClassName} } from '../${appClassName}.js';
import { appTemplate } from '../template/app.template.js';
export class ${shellClassName} extends MatrixActorHtmlElement {
static override accepts = {
...${appClassName}.accepts,
};
static override emits = {
...${appClassName}.emits,
};
static override template = appTemplate;
static getActorClass() {
return ${appClassName};
}
async onBootstrap(): Promise<void> {
const root = this.shadowRoot!;
// Wire up button handlers
root.getElementById('incrementBtn')?.addEventListener('click', () => {
this.sendToSelf('increment', {});
});
root.getElementById('decrementBtn')?.addEventListener('click', () => {
this.sendToSelf('decrement', {});
});
root.getElementById('resetBtn')?.addEventListener('click', () => {
this.sendToSelf('reset', {});
});
root.getElementById('introspectBtn')?.addEventListener('click', () => {
this.sendToSelf('$introspect', {});
});
// Request initial count
this.sendToSelf('getCount', {});
}
onCountChanged(payload: { count: number }): void {
const el = this.shadowRoot?.getElementById('countDisplay');
if (el) el.textContent = String(payload.count);
}
}
if (!customElements.get('${tagName}')) {
customElements.define('${tagName}', ${shellClassName});
}
`;
}
function templateFileContent(appName: string): string {
return `import { appStyles } from '../styles/app.styles.ts';
export const appTemplate = \`
<div class="app-container">
<header class="app-header">
<h1>${appName}</h1>
<div id="transportIndicator" class="transport-indicator">
<span id="transportText">Connecting...</span>
</div>
</header>
<main class="app-main">
<div class="counter-card">
<div class="counter-display">
<span class="counter-label">Count:</span>
<span id="countDisplay" class="counter-value">0</span>
</div>
<div class="counter-controls">
<button id="decrementBtn" class="btn btn-secondary" type="button">-</button>
<button id="incrementBtn" class="btn btn-primary" type="button">+</button>
<button id="resetBtn" class="btn btn-outline" type="button">Reset</button>
</div>
</div>
<div class="info-card">
<p>This app demonstrates the Matrix Shell pattern:</p>
<ul>
<li><strong>Shell</strong> sends commands via <code>sendToSelf()</code></li>
<li><strong>Actor</strong> processes commands and emits events</li>
<li><strong>Shell</strong> reacts to events via <code>on&lt;Event&gt;()</code> handlers</li>
</ul>
<button id="introspectBtn" class="btn btn-outline" type="button">Introspect Actor</button>
</div>
</main>
</div>
<style>\${appStyles}</style>
\`;
`;
}
function stylesFileContent(): string {
return `export const appStyles = \`
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #e0e0e0;
background: #1a1a2e;
min-height: 100vh;
}
.app-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #333;
}
.app-header h1 {
margin: 0;
font-size: 1.5rem;
color: #00d4ff;
}
.transport-indicator {
font-size: 0.8rem;
padding: 4px 12px;
border-radius: 12px;
background: #2a2a4a;
}
.counter-card, .info-card {
background: #16213e;
border: 1px solid #2a2a4a;
border-radius: 8px;
padding: 2rem;
margin-bottom: 1.5rem;
}
.counter-display {
text-align: center;
margin-bottom: 1.5rem;
}
.counter-label {
font-size: 1rem;
color: #888;
margin-right: 0.5rem;
}
.counter-value {
font-size: 3rem;
font-weight: bold;
color: #00d4ff;
font-variant-numeric: tabular-nums;
}
.counter-controls {
display: flex;
justify-content: center;
gap: 1rem;
}
.btn {
padding: 8px 20px;
border-radius: 6px;
border: 1px solid #444;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.btn-primary {
background: #00d4ff;
color: #000;
border-color: #00d4ff;
}
.btn-primary:hover { background: #00b8e6; }
.btn-secondary {
background: #ff6b6b;
color: #fff;
border-color: #ff6b6b;
}
.btn-secondary:hover { background: #e55a5a; }
.btn-outline {
background: transparent;
color: #aaa;
border-color: #555;
}
.btn-outline:hover { border-color: #00d4ff; color: #00d4ff; }
.info-card p { margin-top: 0; color: #999; }
.info-card ul { color: #bbb; line-height: 1.8; }
.info-card code {
background: #2a2a4a;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.85em;
color: #00d4ff;
}
\`;
`;
}
function indexHtmlTemplate(tagName: string, appName: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${appName}</title>
<style>
body { margin: 0; background: #1a1a2e; }
#loading {
color: #888;
font-family: sans-serif;
text-align: center;
padding: 4rem;
}
</style>
</head>
<body>
<div id="loading">Loading ${appName}...</div>
<${tagName} id="app" style="display:none"></${tagName}>
<script type="module" src="./src/init.ts"></script>
</body>
</html>
`;
}
function initTemplate(tagName: string): string {
return `/**
* App initialization sets up Matrix runtime and federation.
*/
import { MatrixRuntime, BrowserDomStrategy, TransportProxy } from '@open-matrix/core';
import {
getBrowserRouter,
createNatsTransport as createFederationNatsTransport,
} from '@open-matrix/federation';
// Register custom elements
import './index.ts';
const loadingEl = document.getElementById('loading')!;
const appEl = document.getElementById('app') as any;
async function init() {
try {
const urlParams = new URLSearchParams(window.location.search);
const root = urlParams.get('root') || 'APP-ROOT';
const backboneUrl = urlParams.get('backboneUrl') || 'nats://hub.example.com:4222';
const allowLocalFallback = ['1', 'true'].includes(
(urlParams.get('allowLocalFallback') || 'true').toLowerCase()
);
console.log('[App] Initializing...');
loadingEl.textContent = 'Initializing Matrix runtime...';
// Get browser router from federation package
const { router, adapter, root: tabRoot } = getBrowserRouter({
baseRoot: root,
});
console.log('[App] Tab root:', tabRoot.value);
// Connect to NATS hub backbone for cross-root communication
loadingEl.textContent = 'Connecting to federation backbone...';
try {
const natsModule = await import('nats');
const nats = natsModule.default || natsModule;
const connect = typeof nats === 'function' ? nats
: typeof nats.connect === 'function' ? nats.connect
: natsModule.connect;
const clientId = \`app-\${Date.now()}-\${Math.random().toString(36).slice(2)}\`;
const client = connect(backboneUrl, {
clientId,
clean: true,
reconnectPeriod: 1000,
connectTimeout: 10000,
});
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Connection timeout')), 10000);
client.on('connect', () => { clearTimeout(timeout); resolve(); });
client.on('error', (err: Error) => { clearTimeout(timeout); reject(err); });
});
const backbone = createFederationNatsTransport(client as any, { jsonMode: false, qos: 0 });
router.setBackbone(backbone);
console.log('[App] Backbone connected');
} catch (err: any) {
if (!allowLocalFallback) throw err;
console.warn('[App] Backbone connection failed, local-only mode:', err?.message);
}
// Create runtime
const transportProxy = new TransportProxy(adapter as any);
const runtime = new MatrixRuntime({
transport: transportProxy,
domStrategy: new BrowserDomStrategy(),
});
const context = runtime.getRootContext();
const pageContext = context.derive('app-page');
// Register services so shell components can find them
const SESSION_INFO_KEY = Symbol.for('matrix.session.info');
const FEDERATION_PEER_KEY = Symbol.for('matrix.federation.peer');
context.setService(SESSION_INFO_KEY, {
mode: 'router',
root: tabRoot.value,
hasBackbone: router.hasBackbone,
});
context.setService(FEDERATION_PEER_KEY, router);
// Mount the app
await appEl.mount(pageContext);
// Update transport indicator
const textEl = appEl.shadowRoot?.getElementById('transportText');
if (textEl) {
textEl.textContent = router.hasBackbone
? \`Federation (\${tabRoot.value})\`
: \`Local-only (\${tabRoot.value})\`;
}
// Show app, hide loading
loadingEl.style.display = 'none';
appEl.style.display = 'block';
console.log('[App] Ready!');
console.log('[App] Root:', tabRoot.value);
console.log('[App] Backbone:', router.hasBackbone ? 'connected' : 'local-only');
} catch (error: any) {
console.error('[App] Initialization failed:', error);
loadingEl.outerHTML = \`
<div style="color: #ff6b6b; font-family: sans-serif; text-align: center; padding: 4rem;">
<strong>Failed to initialize</strong>
<p>\${error.message}</p>
</div>
\`;
}
}
init();
`;
}
function indexTsTemplate(shellClassName: string): string {
return `export { ${shellClassName} } from './shell/${shellClassName}.js';
`;
}
function viteConfigTemplate(appName: string): string {
return `import { defineConfig } from 'vite';
import * as path from 'node:path';
export default defineConfig({
root: '.',
base: '/apps/${appName}/',
build: {
outDir: 'dist',
target: 'es2022',
sourcemap: true,
},
resolve: {
alias: {
// In development, resolve to monorepo source.
// For standalone use, install these as npm packages instead.
'@open-matrix/core': path.resolve(__dirname, '../../src/index.ts'),
},
},
optimizeDeps: {
include: ['nats'],
},
});
`;
}
function testTemplate(appClassName: string, shellClassName: string): string {
return `import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { ${appClassName} } from '../src/${appClassName}.ts';
describe('${appClassName}', () => {
it('declares counter commands', () => {
assert.ok('increment' in ${appClassName}.accepts);
assert.ok('decrement' in ${appClassName}.accepts);
assert.ok('getCount' in ${appClassName}.accepts);
assert.ok('reset' in ${appClassName}.accepts);
});
it('declares countChanged event', () => {
assert.ok('countChanged' in ${appClassName}.emits);
});
});
`;
}
// ── Main command ─────────────────────────────────────────────────────────
export async function webappCommand(
name: string,
options: IWebappCommandOptions = {},
): Promise<IWebappCommandResult> {
const normalizedName = toKebabCase(name);
if (!normalizedName) {
throw new Error('Webapp name must not be empty');
}
const baseName = toPascalCase(normalizedName);
const appClassName = `${baseName}App`;
const shellClassName = `${baseName}AppShell`;
const tagName = `${normalizedName}-app`;
const appName = baseName;
const cwd = path.resolve(options.cwd ?? process.cwd());
const rootDir = path.resolve(options.outputDir ?? path.join(cwd, normalizedName));
// Create directory structure
fs.mkdirSync(path.join(rootDir, 'src', 'shell'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'src', 'template'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'src', 'styles'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'examples'), { recursive: true });
fs.mkdirSync(path.join(rootDir, 'tests'), { recursive: true });
// matrix.json
const manifest = {
name: `@open-matrix/${normalizedName}`,
version: '0.1.0',
description: `${appName} — a Matrix web application`,
runtime: { language: 'typescript', entry: 'dist/index.js' },
webapp: { distDir: 'dist', entry: 'index.html', appName: normalizedName },
components: [],
permissions: { fsPolicy: 'none' },
};
const validation = validateMatrixManifest(manifest);
if (!validation.valid) {
throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`);
}
writeJsonFile(path.join(rootDir, 'matrix.json'), manifest);
writeJsonFile(path.join(rootDir, 'package.json'), generatePackageJson(normalizedName, { webapp: true }));
writeJsonFile(path.join(rootDir, 'tsconfig.json'), generateTsconfig({ webapp: true }));
// Vite config
fs.writeFileSync(path.join(rootDir, 'vite.config.ts'), viteConfigTemplate(normalizedName), 'utf8');
// index.html (entry point)
fs.writeFileSync(path.join(rootDir, 'index.html'), indexHtmlTemplate(tagName, appName), 'utf8');
// Source files
fs.writeFileSync(path.join(rootDir, 'src', 'index.ts'), indexTsTemplate(shellClassName), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', 'init.ts'), initTemplate(tagName), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', `${appClassName}.ts`), appActorTemplate(appClassName), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', 'shell', `${shellClassName}.ts`), shellTemplate(shellClassName, appClassName, tagName), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', 'template', 'app.template.ts'), templateFileContent(appName), 'utf8');
fs.writeFileSync(path.join(rootDir, 'src', 'styles', 'app.styles.ts'), stylesFileContent(), 'utf8');
// Example
writeJsonFile(path.join(rootDir, 'examples', 'introspect.mxq'), {
label: 'Introspect App',
operation: '$introspect',
payload: {},
description: `Introspect the ${appName} actor`,
});
// Test
fs.writeFileSync(path.join(rootDir, 'tests', `${normalizedName}.spec.ts`), testTemplate(appClassName, shellClassName), 'utf8');
console.log(`Created webapp scaffold at ${rootDir}`);
console.log(` Tag: <${tagName}>`);
console.log(` Actor: ${appClassName}`);
console.log(` Shell: ${shellClassName}`);
return { rootDir, appClassName, shellClassName, tagName, appName: normalizedName };
}

805
packages/cli/src/index.ts Normal file
View File

@ -0,0 +1,805 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { pathToFileURL } from 'node:url';
import { initCommand } from './commands/init.js';
import { actorCommand } from './commands/actor.js';
import { actorRunCommand, packageRunCommand } from './commands/actor-run.js';
import { elementCommand } from './commands/element.js';
import { actorElementCommand } from './commands/actor-element.js';
import { cqrsCommand } from './commands/cqrs.js';
import { webappCommand } from './commands/webapp.js';
import { installCommand } from './commands/install.js';
import { uninstallCommand } from './commands/uninstall.js';
import { listCommand } from './commands/list.js';
import { infoCommand } from './commands/info.js';
import { validateCommand } from './commands/validate.js';
import { packCommand } from './commands/pack.js';
import { forkCommand } from './commands/fork.js';
import { publishCommand } from './commands/publish.js';
import { loginCommand, logoutCommand, whoamiCommand } from './commands/auth.js';
import {
configGetCommand,
configListCommand,
configResetCommand,
configSetCommand,
} from './commands/config.js';
import {
registryDoctorCommand,
registryLoginCommand,
registryUseCommand,
} from './commands/registry.js';
import { outdatedCommand } from './commands/outdated.js';
import { updateForkCommand } from './commands/update-fork.js';
import { searchCommand } from './commands/search.js';
function fail(err: unknown): never {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
export function createProgram(): Command {
const program = new Command();
program
.name('matrix')
.description('Matrix SDK CLI for package authors')
.version('0.1.0');
program
.command('init')
.description('Initialize a new Matrix package')
.option('--name <name>', 'Package name override')
.option('--version <version>', 'Package version override')
.option('--description <description>', 'Package description override')
.option('-y, --yes', 'Skip prompts and use defaults/options')
.action(async (options: {
name?: string;
version?: string;
description?: string;
yes?: boolean;
}) => {
await initCommand({
name: options.name,
version: options.version,
description: options.description,
yes: options.yes,
});
});
const actor = program
.command('actor [name]')
.description('Scaffold a new actor package')
.option('--out <path>', 'Output directory (default: ./<name>)')
.action(async (name: string | undefined, options: { out?: string }) => {
if (!name) {
fail(new Error('actor requires a name. Use `matrix actor run <entry>` for direct actor execution.'));
}
await actorCommand(name, { outputDir: options.out });
});
actor
.command('run <entry>')
.description('Run one actor directly on the selected Space bus without managed host inventory')
.option('--export <name>', 'Actor export name (default: default)')
.requiredOption('--mount <mount>', 'Logical actor mount under the selected Space authority root')
.option('--nats-url <url>', 'NATS broker URL, e.g. nats://127.0.0.1:4222')
.option('--root <root>', 'Matrix/NATS subject root')
.option('--space <root>', 'Alias for --root')
.option('--jwt <jwt>', 'NATS user JWT')
.option('--seed <seed>', 'NATS user seed')
.option('--home <path>', 'Credential/config home (also checks ./.matrix and ~/.matrix)')
.option('--props <json>', 'JSON object assigned as actor bootstrap props')
.option('--check-op <op>', 'Invoke one actor operation after mount, print the result, and exit')
.option('--check-payload <json>', 'JSON object passed to --check-op')
.option('--json', 'Output mount details as JSON')
.action(async (entry: string, options: {
export?: string;
mount: string;
natsUrl?: string;
root?: string;
space?: string;
jwt?: string;
seed?: string;
home?: string;
props?: string;
checkOp?: string;
checkPayload?: string;
json?: boolean;
}) => {
try {
await actorRunCommand(entry, options);
} catch (err) {
fail(err);
}
});
const packageCommand = program
.command('package')
.description('Direct package execution commands');
packageCommand
.command('run <packageDir>')
.description('Run one package directly on the selected Space bus without managed host inventory')
.option('--entry <path>', 'Package entry override')
.option('--export <name>', 'Package export override')
.option('--mount <mount>', 'Logical actor mount under the selected Space authority root')
.option('--nats-url <url>', 'NATS broker URL, e.g. nats://127.0.0.1:4222')
.option('--root <root>', 'Matrix/NATS subject root')
.option('--space <root>', 'Alias for --root')
.option('--jwt <jwt>', 'NATS user JWT')
.option('--seed <seed>', 'NATS user seed')
.option('--home <path>', 'Credential/config home (also checks ./.matrix and ~/.matrix)')
.option('--overrides <json>', 'JSON object passed as package bootstrap overrides')
.option('--check-op <op>', 'Invoke one actor operation after package mount, print the result, and exit')
.option('--check-payload <json>', 'JSON object passed to --check-op')
.option('--json', 'Output mount details as JSON')
.action(async (packageDir: string, options: {
entry?: string;
export?: string;
mount?: string;
natsUrl?: string;
root?: string;
space?: string;
jwt?: string;
seed?: string;
home?: string;
overrides?: string;
checkOp?: string;
checkPayload?: string;
json?: boolean;
}) => {
try {
await packageRunCommand(packageDir, options);
} catch (err) {
fail(err);
}
});
program
.command('element <name>')
.description('Scaffold a new element package')
.option('--out <path>', 'Output directory (default: ./<name>)')
.action(async (name: string, options: { out?: string }) => {
await elementCommand(name, { outputDir: options.out });
});
program
.command('actor-element <name>')
.description('Scaffold a package containing paired actor + element components')
.option('--out <path>', 'Output directory (default: ./<name>)')
.action(async (name: string, options: { out?: string }) => {
await actorElementCommand(name, { outputDir: options.out });
});
program
.command('cqrs <name>')
.description('Scaffold a package with CQRS command/query actors')
.option('--out <path>', 'Output directory (default: ./<name>)')
.action(async (name: string, options: { out?: string }) => {
await cqrsCommand(name, { outputDir: options.out });
});
program
.command('webapp <name>')
.description('Scaffold a browser web app with Shell pattern, federation, and Vite')
.option('--out <path>', 'Output directory (default: ./<name>)')
.action(async (name: string, options: { out?: string }) => {
await webappCommand(name, { outputDir: options.out });
});
program
.command('pack')
.description('Create distributable tarball for current package')
.option('--out-dir <path>', 'Output directory for tarball')
.option('--build-first', 'Run npm build before packing')
.option('--json', 'Output as JSON')
.action(async (options: { outDir?: string; buildFirst?: boolean; json?: boolean }) => {
try {
const result = await packCommand({
outDir: options.outDir,
buildFirst: options.buildFirst,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Packed ${result.packageName}@${result.version} -> ${result.tarballPath}`);
} catch (err) {
fail(err);
}
});
program
.command('install <source>')
.description('Install a Matrix package from directory, tarball, or registry name')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--registry-dir <path>', 'Override local registry root')
.option('--registry <url>', 'NPM-compatible registry URL')
.option('--credentials-file <path>', 'Override registry credentials file path')
.option('-f, --force', 'Overwrite existing installed package')
.option('--json', 'Output as JSON')
.action(async (source: string, options: {
packagesDir?: string;
registryDir?: string;
registry?: string;
credentialsFile?: string;
force?: boolean;
json?: boolean;
}) => {
try {
const result = await installCommand(source, {
packagesDir: options.packagesDir,
registryDir: options.registryDir,
registry: options.registry,
credentialsPath: options.credentialsFile,
force: options.force,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Installed ${result.packageName}@${result.version} -> ${result.targetDir}`);
} catch (err) {
fail(err);
}
});
program
.command('uninstall <packageName>')
.description('Uninstall a Matrix package from package root')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--json', 'Output as JSON')
.action(async (packageName: string, options: { packagesDir?: string; json?: boolean }) => {
try {
const result = await uninstallCommand(packageName, {
packagesDir: options.packagesDir,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Uninstalled ${result.packageName}`);
} catch (err) {
fail(err);
}
});
program
.command('fork <packageName>')
.description('Fork an installed package for modification')
.option('--name <newName>', 'Forked package name')
.option('--replace-origin', 'Do not rename component type/mount/export fields')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--forked-by <name>', 'Fork author metadata override')
.option('--json', 'Output as JSON')
.action(async (packageName: string, options: {
name?: string;
replaceOrigin?: boolean;
packagesDir?: string;
forkedBy?: string;
json?: boolean;
}) => {
try {
const result = await forkCommand(packageName, {
name: options.name,
replaceOrigin: options.replaceOrigin,
packagesDir: options.packagesDir,
forkedBy: options.forkedBy,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Forked ${result.sourcePackageName} -> ${result.forkedPackageName}`);
} catch (err) {
fail(err);
}
});
program
.command('publish')
.description('Publish current package to registry')
.option('--registry-dir <path>', 'Override local registry root')
.option('--registry <url>', 'NPM-compatible registry URL')
.option('--credentials-file <path>', 'Override credentials file path')
.option('--dry-run', 'Run publish preflight without writing registry artifacts')
.option('--json', 'Output as JSON')
.action(async (options: {
registryDir?: string;
registry?: string;
credentialsFile?: string;
dryRun?: boolean;
json?: boolean;
}) => {
try {
const result = await publishCommand({
registryDir: options.registryDir,
registry: options.registry,
credentialsPath: options.credentialsFile,
dryRun: options.dryRun,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Published ${result.packageName}@${result.version}`);
} catch (err) {
fail(err);
}
});
program
.command('search <query>')
.description('Search the configured npm-compatible Matrix package registry')
.option('--registry-dir <path>', 'Search local Matrix discovery index under this registry root')
.option('--registry <url>', 'NPM-compatible registry URL')
.option('--credentials-file <path>', 'Override registry credentials file path')
.option('--limit <count>', 'Maximum number of results (default: 20)', parseInt)
.option('--json', 'Output as JSON')
.action(async (query: string, options: {
registryDir?: string;
registry?: string;
credentialsFile?: string;
limit?: number;
json?: boolean;
}) => {
try {
const result = await searchCommand(query, {
registryDir: options.registryDir,
registry: options.registry,
credentialsPath: options.credentialsFile,
limit: options.limit,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
if (result.results.length === 0) {
console.log('(no packages found)');
return;
}
for (const entry of result.results) {
const description = entry.description ? ` - ${entry.description}` : '';
console.log(`${entry.packageName}@${entry.version}${description}`);
}
} catch (err) {
fail(err);
}
});
const config = program
.command('config')
.description('Read and write Matrix CLI configuration');
config
.command('get <key>')
.description('Read a Matrix CLI config value')
.option('--config-file <path>', 'Override CLI config file path')
.option('--json', 'Output as JSON')
.action(async (key: string, options: { configFile?: string; json?: boolean }) => {
try {
const result = await configGetCommand(key, {
configPath: options.configFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(result.value ?? '');
} catch (err) {
fail(err);
}
});
config
.command('set <key> <value>')
.description('Set a Matrix CLI config value')
.option('--config-file <path>', 'Override CLI config file path')
.option('--json', 'Output as JSON')
.action(async (key: string, value: string, options: { configFile?: string; json?: boolean }) => {
try {
const result = await configSetCommand(key, value, {
configPath: options.configFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`${result.key}=${result.value}`);
} catch (err) {
fail(err);
}
});
config
.command('list')
.description('List Matrix CLI config values')
.option('--config-file <path>', 'Override CLI config file path')
.option('--json', 'Output as JSON')
.action(async (options: { configFile?: string; json?: boolean }) => {
try {
const result = await configListCommand({
configPath: options.configFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`registry=${result.effective.registry}`);
} catch (err) {
fail(err);
}
});
config
.command('reset')
.description('Reset Matrix CLI config values')
.option('--config-file <path>', 'Override CLI config file path')
.option('--json', 'Output as JSON')
.action(async (options: { configFile?: string; json?: boolean }) => {
try {
const result = await configResetCommand({
configPath: options.configFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Reset ${result.configPath}`);
} catch (err) {
fail(err);
}
});
const registry = program
.command('registry')
.description('Inspect and configure the Matrix package registry');
registry
.command('doctor')
.description('Diagnose npm registry and Matrix scope resolution')
.option('--registry <url>', 'Registry URL to diagnose')
.option('--credentials-file <path>', 'Override registry credentials file path')
.option('--config-file <path>', 'Override CLI config file path')
.option('--timeout-ms <ms>', 'Network/command timeout in milliseconds', (value) => Number.parseInt(value, 10))
.option('--json', 'Output as JSON')
.action(async (options: {
registry?: string;
credentialsFile?: string;
configFile?: string;
timeoutMs?: number;
json?: boolean;
}) => {
try {
const result = await registryDoctorCommand({
registry: options.registry,
credentialsPath: options.credentialsFile,
configPath: options.configFile,
timeoutMs: options.timeoutMs,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`registry=${result.registry.url}`);
console.log(`registryReachable=${result.registry.reachable}`);
console.log(`credentials=${result.credentials.found ? result.credentials.username ?? 'found' : 'missing'}`);
console.log(`scopeResolution=${result.npm.allScopesMatch ? 'ok' : 'mismatch'}`);
} catch (err) {
fail(err);
}
});
registry
.command('login')
.description('Store credentials for a Matrix npm registry')
.requiredOption('--username <name>', 'Registry username')
.requiredOption('--token <token>', 'Registry token')
.option('--email <email>', 'Optional account email')
.option('--expires-at <iso>', 'Optional token expiry timestamp')
.option('--registry <url>', 'Registry URL')
.option('--credentials-file <path>', 'Override registry credentials file path')
.option('--json', 'Output as JSON')
.action(async (options: {
username: string;
token: string;
email?: string;
expiresAt?: string;
registry?: string;
credentialsFile?: string;
json?: boolean;
}) => {
try {
const result = await registryLoginCommand({
username: options.username,
token: options.token,
email: options.email,
expiresAt: options.expiresAt,
registry: options.registry,
credentialsPath: options.credentialsFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Logged in as ${result.username} (${result.registry})`);
} catch (err) {
fail(err);
}
});
registry
.command('use <profile>')
.description('Set the Matrix package registry profile')
.option('--registry <url>', 'Registry URL override')
.option('--config-file <path>', 'Override CLI config file path')
.option('--json', 'Output as JSON')
.action(async (profile: string, options: {
registry?: string;
configFile?: string;
json?: boolean;
}) => {
try {
const result = await registryUseCommand(profile, {
registry: options.registry,
configPath: options.configFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`registry=${result.registry}`);
} catch (err) {
fail(err);
}
});
program
.command('login')
.description('Authenticate with a Matrix package registry')
.option('--username <name>', 'Registry username')
.option('--token <token>', 'Registry token')
.option('--email <email>', 'Optional account email')
.option('--expires-at <iso>', 'Optional token expiry timestamp')
.option('--registry <url>', 'Registry identifier')
.option('--credentials-file <path>', 'Override credentials file path')
.option('--json', 'Output as JSON')
.action(async (options: {
username: string;
token: string;
email?: string;
expiresAt?: string;
registry?: string;
credentialsFile?: string;
json?: boolean;
}) => {
try {
if (!options.username || !options.token) {
throw new Error('MX_AUTH_REQUIRED: username and token are required for registry login');
}
const result = await loginCommand({
username: options.username,
token: options.token,
email: options.email,
expiresAt: options.expiresAt,
registry: options.registry,
credentialsPath: options.credentialsFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`Logged in as ${result.username} (${result.registry})`);
} catch (err) {
fail(err);
}
});
program
.command('logout')
.description('Clear stored registry credentials')
.option('--registry <url>', 'Registry identifier')
.option('--credentials-file <path>', 'Override credentials file path')
.option('--json', 'Output as JSON')
.action(async (options: {
registry?: string;
credentialsFile?: string;
json?: boolean;
}) => {
try {
const result = await logoutCommand({
registry: options.registry,
credentialsPath: options.credentialsFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(result.removed ? `Logged out of ${result.registry}` : `No session for ${result.registry}`);
} catch (err) {
fail(err);
}
});
program
.command('whoami')
.description('Show current registry identity')
.option('--registry <url>', 'Registry identifier')
.option('--credentials-file <path>', 'Override credentials file path')
.option('--json', 'Output as JSON')
.action(async (options: {
registry?: string;
credentialsFile?: string;
json?: boolean;
}) => {
try {
const result = await whoamiCommand({
registry: options.registry,
credentialsPath: options.credentialsFile,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
if (!result.authenticated) {
console.log(`Not logged in (${result.registry})`);
return;
}
console.log(`${result.username} (${result.registry})`);
} catch (err) {
fail(err);
}
});
program
.command('outdated')
.description('Check if forked packages have upstream updates')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--registry-dir <path>', 'Override local registry root')
.option('--json', 'Output as JSON')
.action(async (options: { packagesDir?: string; registryDir?: string; json?: boolean }) => {
try {
const result = await outdatedCommand({
packagesDir: options.packagesDir,
registryDir: options.registryDir,
});
if (options.json) {
console.log(JSON.stringify(result.entries, null, 2));
return;
}
if (result.entries.length === 0) {
console.log('(no forked packages)');
return;
}
for (const entry of result.entries) {
console.log(`${entry.packageName} ${entry.localOriginVersion} -> ${entry.latestOriginVersion ?? 'n/a'} (${entry.status})`);
}
} catch (err) {
fail(err);
}
});
program
.command('update-fork <packageName>')
.description('Update fork metadata to latest upstream version')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--registry-dir <path>', 'Override local registry root')
.option('--version <version>', 'Target upstream version')
.option('--json', 'Output as JSON')
.action(async (packageName: string, options: {
packagesDir?: string;
registryDir?: string;
version?: string;
json?: boolean;
}) => {
try {
const result = await updateForkCommand(packageName, {
packagesDir: options.packagesDir,
registryDir: options.registryDir,
version: options.version,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
if (result.updated) {
console.log(`Updated ${result.packageName}: ${result.previousOriginVersion} -> ${result.nextOriginVersion}`);
} else {
console.log(`${result.packageName} already up to date (${result.nextOriginVersion})`);
}
} catch (err) {
fail(err);
}
});
program
.command('list')
.description('List installed Matrix packages')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--json', 'Output as JSON')
.action(async (options: { packagesDir?: string; json?: boolean }) => {
try {
const result = await listCommand({
packagesDir: options.packagesDir,
});
if (options.json) {
console.log(JSON.stringify(result.packages, null, 2));
return;
}
if (result.packages.length === 0) {
console.log('(no installed packages)');
return;
}
for (const packageName of result.packages) {
console.log(packageName);
}
} catch (err) {
fail(err);
}
});
program
.command('info <packageName>')
.description('Show installed package metadata')
.option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
.option('--json', 'Output as JSON')
.action(async (packageName: string, options: { packagesDir?: string; json?: boolean }) => {
try {
const result = await infoCommand(packageName, {
packagesDir: options.packagesDir,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`${result.packageName}`);
console.log(`path: ${result.installPath}`);
console.log(`manifest: ${result.hasMatrixJson ? 'matrix.json' : 'package.json.matrix'}`);
} catch (err) {
fail(err);
}
});
program
.command('validate [target]')
.description('Validate matrix manifest at directory, matrix.json, or package.json')
.option('--json', 'Output as JSON')
.action(async (target: string | undefined, options: { json?: boolean }) => {
try {
const result = await validateCommand(target);
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`package: ${result.packageName}`);
console.log(`valid: ${result.valid ? 'yes' : 'no'}`);
if (!result.valid) {
for (const diagnostic of result.diagnostics) {
console.log(`- ${diagnostic.code}: ${diagnostic.message}`);
}
}
}
if (!result.valid) {
process.exit(1);
}
} catch (err) {
fail(err);
}
});
return program;
}
async function main(): Promise<void> {
const program = createProgram();
await program.parseAsync(process.argv);
}
const invokedPath = process.argv[1];
const isDirectExecution = Boolean(invokedPath)
&& pathToFileURL(invokedPath).href === import.meta.url;
if (isDirectExecution) {
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
}

View File

@ -0,0 +1,73 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { spawnSync } from 'node:child_process';
const DEFAULT_EXCLUDES = [
'node_modules',
'.git',
'tests',
'.forked-from',
'.env',
'.env.*',
'*.spec.ts',
'*.test.ts',
];
function runTar(args: string[]): void {
const result = spawnSync('tar', args, {
encoding: 'utf8',
windowsHide: true,
});
if (result.status !== 0) {
throw new Error(`tar failed: ${result.stderr || result.stdout || `exit ${result.status ?? 'unknown'}`}`);
}
}
export function createTarball(sourceDir: string, tarballPath: string, excludes: string[] = DEFAULT_EXCLUDES): void {
fs.mkdirSync(path.dirname(tarballPath), { recursive: true });
const excludeArgs = excludes.flatMap((pattern) => ['--exclude', pattern]);
runTar([
'-czf',
tarballPath,
...excludeArgs,
'-C',
sourceDir,
'.',
]);
}
export function extractTarball(tarballPath: string, outputDir: string): void {
fs.mkdirSync(outputDir, { recursive: true });
runTar([
'-xzf',
tarballPath,
'-C',
outputDir,
]);
}
export function computeDirectorySha256(rootDir: string, excludes: Set<string> = new Set(['node_modules', '.git'])): string {
const hash = crypto.createHash('sha256');
function walk(dir: string): void {
const entries = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => !excludes.has(entry.name))
.sort((a, b) => a.name.localeCompare(b.name));
for (const entry of entries) {
const absolute = path.join(dir, entry.name);
const relative = path.relative(rootDir, absolute).split(path.sep).join('/');
if (entry.isDirectory()) {
hash.update(`dir:${relative}\n`);
walk(absolute);
} else if (entry.isFile()) {
hash.update(`file:${relative}\n`);
hash.update(fs.readFileSync(absolute));
}
}
}
walk(rootDir);
return hash.digest('hex');
}

View File

@ -0,0 +1,207 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { resolvePackageRegistry } from './config-store.js';
type JsonRecord = Record<string, unknown>;
export interface IRegistryCredential {
readonly token: string;
readonly username: string;
readonly email?: string;
readonly expiresAt?: string;
}
interface ICredentialFile {
readonly registries: Record<string, IRegistryCredential>;
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function matrixHome(cwd: string): string {
return path.resolve(
cwd,
process.env.MATRIX_HOME
?? path.join(cwd, '.matrix'),
);
}
function resolveCredentialPath(cwd: string, credentialsPath?: string): string {
return path.resolve(
cwd,
credentialsPath ?? path.join(matrixHome(cwd), 'credentials', 'registry.json'),
);
}
function readCredentialFile(filePath: string): ICredentialFile {
if (!fs.existsSync(filePath)) {
return { registries: {} };
}
if (fs.statSync(filePath).isDirectory()) {
throw new Error(`MX_AUTH_CREDENTIALS_PATH_IS_DIRECTORY: ${filePath}`);
}
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
const record = asRecord(parsed);
const registries = asRecord(record?.registries) ?? {};
const normalized: Record<string, IRegistryCredential> = {};
for (const [registry, value] of Object.entries(registries)) {
const entry = asRecord(value);
if (!entry) continue;
if (typeof entry.token !== 'string' || typeof entry.username !== 'string') continue;
normalized[registry] = {
token: entry.token,
username: entry.username,
email: typeof entry.email === 'string' ? entry.email : undefined,
expiresAt: typeof entry.expiresAt === 'string' ? entry.expiresAt : undefined,
};
}
return { registries: normalized };
}
function writeCredentialFile(filePath: string, payload: ICredentialFile): void {
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
throw new Error(`MX_AUTH_CREDENTIALS_PATH_IS_DIRECTORY: ${filePath}`);
}
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
}
function readNpmTokenFromEnvironment(): string | null {
const token = process.env.NODE_AUTH_TOKEN ?? process.env.NPM_TOKEN;
if (typeof token !== 'string') {
return null;
}
const trimmed = token.trim();
return trimmed.length > 0 ? trimmed : null;
}
function configuredNpmUserConfigPath(): string | undefined {
const value = process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig;
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function npmrcSearchPaths(cwd: string): readonly string[] {
const configuredUserConfig = configuredNpmUserConfigPath();
if (configuredUserConfig) {
return [configuredUserConfig];
}
return [
path.join(cwd, '.npmrc'),
path.join(os.homedir(), '.npmrc'),
];
}
function readNpmTokenFromNpmrc(filePath: string, registry: string): string | null {
if (!fs.existsSync(filePath)) {
return null;
}
const lines = fs.readFileSync(filePath, 'utf8')
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith('#') && !line.startsWith(';'));
const registryKeys = npmRegistryAuthKeys(registry);
for (const line of lines) {
for (const key of registryKeys) {
const prefix = `${key}:_authToken=`;
if (line.startsWith(prefix)) {
const token = line.slice(prefix.length).trim();
return token.length > 0 ? token : null;
}
}
}
return null;
}
function npmRegistryAuthKeys(registry: string): readonly string[] {
const parsed = new URL(registry);
const host = parsed.host;
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
const segments = pathname.split('/').filter((segment) => segment.length > 0);
const keys = new Set<string>();
for (let length = segments.length; length >= 0; length -= 1) {
const partial = length === 0 ? '/' : `/${segments.slice(0, length).join('/')}/`;
keys.add(`//${host}${partial}`);
}
return [...keys];
}
function readNpmCredential(cwd: string, registry: string): IRegistryCredential | null {
const envToken = readNpmTokenFromEnvironment();
if (envToken) {
return { username: 'npm-token', token: envToken };
}
for (const filePath of npmrcSearchPaths(cwd)) {
const token = readNpmTokenFromNpmrc(filePath, registry);
if (token) {
return { username: 'npm-token', token };
}
}
return null;
}
export function saveRegistryCredential(
cwd: string,
credential: IRegistryCredential,
options: {
readonly registry?: string;
readonly credentialsPath?: string;
} = {},
): { readonly registry: string; readonly credentialsPath: string } {
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
const filePath = resolveCredentialPath(cwd, options.credentialsPath);
const payload = readCredentialFile(filePath);
payload.registries[registry] = credential;
writeCredentialFile(filePath, payload);
return { registry, credentialsPath: filePath };
}
export function removeRegistryCredential(
cwd: string,
options: {
readonly registry?: string;
readonly credentialsPath?: string;
} = {},
): { readonly registry: string; readonly removed: boolean; readonly credentialsPath: string } {
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
const filePath = resolveCredentialPath(cwd, options.credentialsPath);
const payload = readCredentialFile(filePath);
const removed = Boolean(payload.registries[registry]);
if (removed) {
delete payload.registries[registry];
writeCredentialFile(filePath, payload);
}
return { registry, removed, credentialsPath: filePath };
}
export function readRegistryCredential(
cwd: string,
options: {
readonly registry?: string;
readonly credentialsPath?: string;
} = {},
): {
readonly registry: string;
readonly credentialsPath: string;
readonly credential: IRegistryCredential | null;
} {
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
const filePath = resolveCredentialPath(cwd, options.credentialsPath);
const payload = readCredentialFile(filePath);
return {
registry,
credentialsPath: filePath,
credential: payload.registries[registry] ?? readNpmCredential(cwd, registry),
};
}

View File

@ -0,0 +1,174 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
type JsonRecord = Record<string, unknown>;
export const DEFAULT_PACKAGE_REGISTRY = 'https://registry.npmjs.org/';
export interface IMatrixCliConfig {
readonly registry?: string;
}
export interface IConfigStoreOptions {
readonly cwd?: string;
readonly configPath?: string;
}
export interface IResolvedRegistryConfig {
readonly registry: string;
readonly source: 'explicit' | 'env' | 'config' | 'default';
readonly configPath: string;
}
export interface IConfigCommandResult {
readonly configPath: string;
readonly values: IMatrixCliConfig;
}
const CONFIG_KEYS = new Set(['registry']);
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeRegistryUrl(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
throw new Error('MX_CONFIG_INVALID: registry must be a non-empty HTTP(S) URL');
}
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
throw new Error(`MX_CONFIG_INVALID: registry must be an HTTP(S) URL (${value})`);
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`MX_CONFIG_INVALID: registry must be an HTTP(S) URL (${value})`);
}
if (!parsed.pathname.endsWith('/')) {
parsed.pathname = `${parsed.pathname}/`;
}
return parsed.toString();
}
function matrixHome(cwd: string): string {
return path.resolve(
cwd,
process.env.MATRIX_HOME
?? path.join(cwd, '.matrix'),
);
}
export function resolveMatrixCliConfigPath(options: IConfigStoreOptions = {}): string {
const cwd = path.resolve(options.cwd ?? process.cwd());
return path.resolve(
cwd,
options.configPath
?? process.env.MATRIX_CLI_CONFIG
?? path.join(matrixHome(cwd), 'config.json'),
);
}
export function readMatrixCliConfig(options: IConfigStoreOptions = {}): IConfigCommandResult {
const configPath = resolveMatrixCliConfigPath(options);
if (!fs.existsSync(configPath)) {
return { configPath, values: {} };
}
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as unknown;
const record = asRecord(parsed);
if (!record) {
throw new Error(`MX_CONFIG_INVALID: ${configPath} must contain a JSON object`);
}
const registry = readOptionalString(record.registry);
return {
configPath,
values: {
...(registry ? { registry: normalizeRegistryUrl(registry) } : {}),
},
};
}
export function writeMatrixCliConfig(
values: IMatrixCliConfig,
options: IConfigStoreOptions = {},
): IConfigCommandResult {
const configPath = resolveMatrixCliConfigPath(options);
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
return { configPath, values };
}
export function setMatrixCliConfigValue(
key: string,
value: string,
options: IConfigStoreOptions = {},
): IConfigCommandResult {
const normalizedKey = key.trim();
if (!CONFIG_KEYS.has(normalizedKey)) {
throw new Error(`MX_CONFIG_INVALID: unsupported config key "${key}"`);
}
const current = readMatrixCliConfig(options);
const next: IMatrixCliConfig = {
...current.values,
registry: normalizeRegistryUrl(value),
};
return writeMatrixCliConfig(next, options);
}
export function resetMatrixCliConfig(options: IConfigStoreOptions = {}): IConfigCommandResult {
const configPath = resolveMatrixCliConfigPath(options);
if (fs.existsSync(configPath)) {
fs.rmSync(configPath, { force: true });
}
return { configPath, values: {} };
}
export function resolvePackageRegistry(
explicitRegistry: string | undefined,
options: IConfigStoreOptions = {},
): IResolvedRegistryConfig {
const configPath = resolveMatrixCliConfigPath(options);
if (explicitRegistry && explicitRegistry.trim().length > 0) {
return {
registry: normalizeRegistryUrl(explicitRegistry),
source: 'explicit',
configPath,
};
}
const envRegistry = process.env.MATRIX_PACKAGE_REGISTRY
?? process.env.MATRIX_REGISTRY_URL;
if (envRegistry && envRegistry.trim().length > 0) {
return {
registry: normalizeRegistryUrl(envRegistry),
source: 'env',
configPath,
};
}
const stored = readMatrixCliConfig(options).values.registry;
if (stored) {
return {
registry: stored,
source: 'config',
configPath,
};
}
return {
registry: DEFAULT_PACKAGE_REGISTRY,
source: 'default',
configPath,
};
}

View File

@ -0,0 +1,538 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
readManifestFromPackageDir,
readPackageJson,
readVersionFromPackageDir,
} from './package-store.js';
type JsonRecord = Record<string, unknown>;
export interface IWebappDiscoveryMetadata {
readonly appName: string;
readonly routePrefix?: string;
readonly displayName?: string;
readonly icon?: string;
readonly navOrder?: number;
readonly description?: string;
readonly shells: readonly string[];
}
export interface IComponentDiscoveryMetadata {
readonly type: string;
readonly exportName?: string;
readonly mount?: string;
readonly surface?: string;
readonly description?: string;
readonly kind?: string;
readonly accepts: readonly string[];
readonly emits: readonly string[];
readonly skills: readonly string[];
readonly tags: readonly string[];
readonly capabilities: readonly string[];
}
export interface IPackageDiscoveryMetadata {
readonly packageName: string;
readonly version: string;
readonly displayName?: string;
readonly description?: string;
readonly declaredKind?: string;
readonly inferredKind: string;
readonly tags: readonly string[];
readonly capabilities: readonly string[];
readonly permissions: JsonRecord | null;
readonly webapp?: IWebappDiscoveryMetadata;
readonly components: readonly IComponentDiscoveryMetadata[];
}
interface IClassStaticMetadata {
readonly className: string;
readonly description?: string;
readonly kind?: string;
readonly accepts: readonly string[];
readonly emits: readonly string[];
readonly skills: readonly string[];
readonly tags: readonly string[];
readonly capabilities: readonly string[];
}
function asRecord(value: unknown): JsonRecord | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as JsonRecord
: null;
}
function readOptionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function readOptionalNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function readStringArray(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
return value
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
}
function readObjectKeys(value: unknown): readonly string[] {
const record = asRecord(value);
return record ? Object.keys(record).sort((left, right) => left.localeCompare(right)) : [];
}
function inferPackageKind(manifest: JsonRecord): string {
const webapp = asRecord(manifest.webapp);
const components = Array.isArray(manifest.components) ? manifest.components : [];
if (webapp && components.length > 0) {
return 'app';
}
if (webapp) {
return 'webapp';
}
if (components.length > 0) {
return 'service';
}
return 'library';
}
function readPackageDescription(packageDir: string, manifest: JsonRecord): string | undefined {
const manifestDescription = readOptionalString(manifest.description);
if (manifestDescription) {
return manifestDescription;
}
try {
return readOptionalString(readPackageJson(packageDir).description);
} catch {
return undefined;
}
}
function readWebappMetadata(manifest: JsonRecord): IWebappDiscoveryMetadata | undefined {
const webapp = asRecord(manifest.webapp);
const appName = readOptionalString(webapp?.appName);
if (!webapp || !appName) {
return undefined;
}
const shells = readStringArray(webapp.shells);
return {
appName,
...(readOptionalString(webapp.routePrefix) ? { routePrefix: readOptionalString(webapp.routePrefix) } : {}),
...(readOptionalString(webapp.displayName) ? { displayName: readOptionalString(webapp.displayName) } : {}),
...(readOptionalString(webapp.icon) ? { icon: readOptionalString(webapp.icon) } : {}),
...(readOptionalNumber(webapp.navOrder) !== undefined ? { navOrder: readOptionalNumber(webapp.navOrder) } : {}),
...(readOptionalString(webapp.description) ? { description: readOptionalString(webapp.description) } : {}),
shells: shells.length > 0 ? shells : ['platform', 'edge'],
};
}
function readComponentManifestEntry(value: unknown): IComponentDiscoveryMetadata | null {
const record = asRecord(value);
const type = readOptionalString(record?.type);
if (!record || !type) {
return null;
}
return {
type,
...(readOptionalString(record.export) ? { exportName: readOptionalString(record.export) } : {}),
...(readOptionalString(record.mount) ? { mount: readOptionalString(record.mount) } : {}),
...(readOptionalString(record.surface) ? { surface: readOptionalString(record.surface) } : {}),
...(readOptionalString(record.description) ? { description: readOptionalString(record.description) } : {}),
...(readOptionalString(record.kind) ? { kind: readOptionalString(record.kind) } : {}),
accepts: readStringArray(record.accepts).length > 0 ? readStringArray(record.accepts) : readObjectKeys(record.accepts),
emits: readStringArray(record.emits).length > 0 ? readStringArray(record.emits) : readObjectKeys(record.emits),
skills: readStringArray(record.skills),
tags: readStringArray(record.tags),
capabilities: readStringArray(record.capabilities),
};
}
function mergeUnique(left: readonly string[], right: readonly string[]): readonly string[] {
return [...new Set([...left, ...right])].sort((a, b) => a.localeCompare(b));
}
function enrichComponent(
component: IComponentDiscoveryMetadata,
classStatics: ReadonlyMap<string, IClassStaticMetadata>,
): IComponentDiscoveryMetadata {
const statics = classStatics.get(component.type)
?? (component.exportName ? classStatics.get(component.exportName) : undefined);
if (!statics) {
return component;
}
return {
...component,
description: component.description ?? statics.description,
kind: component.kind ?? statics.kind,
accepts: mergeUnique(component.accepts, statics.accepts),
emits: mergeUnique(component.emits, statics.emits),
skills: mergeUnique(component.skills, statics.skills),
tags: mergeUnique(component.tags, statics.tags),
capabilities: mergeUnique(component.capabilities, statics.capabilities),
};
}
function readSourceFiles(packageDir: string): readonly string[] {
const roots = ['src'];
const runtimeEntry = readOptionalString(asRecord(readManifestFromPackageDir(packageDir).manifest.runtime)?.entry);
if (runtimeEntry) {
roots.push(path.dirname(runtimeEntry));
}
const sourceFiles: string[] = [];
const visited = new Set<string>();
function walk(dir: string): void {
const resolved = path.resolve(dir);
if (visited.has(resolved) || !fs.existsSync(resolved)) {
return;
}
visited.add(resolved);
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) {
continue;
}
const fullPath = path.join(resolved, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
continue;
}
if (entry.isFile() && /\.(?:ts|tsx|js|mjs|cjs)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
sourceFiles.push(fullPath);
}
}
}
for (const root of roots) {
walk(path.join(packageDir, root));
}
return [...new Set(sourceFiles)].sort((left, right) => left.localeCompare(right));
}
function findMatchingBrace(source: string, openIndex: number): number {
let depth = 0;
let quote: '"' | '\'' | '`' | null = null;
let escaped = false;
let lineComment = false;
let blockComment = false;
for (let index = openIndex; index < source.length; index += 1) {
const char = source[index] ?? '';
const next = source[index + 1] ?? '';
if (lineComment) {
if (char === '\n') lineComment = false;
continue;
}
if (blockComment) {
if (char === '*' && next === '/') {
blockComment = false;
index += 1;
}
continue;
}
if (quote) {
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === quote) {
quote = null;
}
continue;
}
if (char === '/' && next === '/') {
lineComment = true;
index += 1;
continue;
}
if (char === '/' && next === '*') {
blockComment = true;
index += 1;
continue;
}
if (char === '"' || char === '\'' || char === '`') {
quote = char;
continue;
}
if (char === '{') {
depth += 1;
continue;
}
if (char === '}') {
depth -= 1;
if (depth === 0) {
return index;
}
}
}
return -1;
}
function readStaticString(classBody: string, fieldName: string): string | undefined {
const pattern = new RegExp(`static\\s+${fieldName}\\s*(?::[^=;]+)?=\\s*(['"\`])([\\s\\S]*?)\\1`);
const match = pattern.exec(classBody);
return readOptionalString(match?.[2]);
}
function readStaticArray(classBody: string, fieldName: string): readonly string[] {
const fieldIndex = classBody.search(new RegExp(`static\\s+${fieldName}\\s*(?::[^=;]+)?=`));
if (fieldIndex < 0) {
return [];
}
const openIndex = classBody.indexOf('[', fieldIndex);
if (openIndex < 0) {
return [];
}
const closeIndex = findMatchingBracket(classBody, openIndex, '[', ']');
if (closeIndex < 0) {
return [];
}
const body = classBody.slice(openIndex + 1, closeIndex);
const values: string[] = [];
const stringPattern = /(['"`])([\s\S]*?)\1/g;
let match: RegExpExecArray | null;
while ((match = stringPattern.exec(body)) !== null) {
const value = readOptionalString(match[2]);
if (value) values.push(value);
}
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}
function findMatchingBracket(
source: string,
openIndex: number,
openToken: '[' | '{',
closeToken: ']' | '}',
): number {
let depth = 0;
let quote: '"' | '\'' | '`' | null = null;
let escaped = false;
for (let index = openIndex; index < source.length; index += 1) {
const char = source[index] ?? '';
if (quote) {
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === quote) {
quote = null;
}
continue;
}
if (char === '"' || char === '\'' || char === '`') {
quote = char;
continue;
}
if (char === openToken) {
depth += 1;
continue;
}
if (char === closeToken) {
depth -= 1;
if (depth === 0) {
return index;
}
}
}
return -1;
}
function readStaticObjectKeys(classBody: string, fieldName: string): readonly string[] {
const fieldIndex = classBody.search(new RegExp(`static\\s+${fieldName}\\s*(?::[^=;]+)?=`));
if (fieldIndex < 0) {
return [];
}
const openIndex = classBody.indexOf('{', fieldIndex);
if (openIndex < 0) {
return [];
}
const closeIndex = findMatchingBracket(classBody, openIndex, '{', '}');
if (closeIndex < 0) {
return [];
}
const objectText = classBody.slice(openIndex + 1, closeIndex);
return readTopLevelObjectKeys(objectText);
}
function readTopLevelObjectKeys(objectText: string): readonly string[] {
const keys: string[] = [];
let index = 0;
let depth = 0;
let quote: '"' | '\'' | '`' | null = null;
let escaped = false;
let expectingKey = true;
while (index < objectText.length) {
const char = objectText[index] ?? '';
if (quote) {
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
quote = null;
}
index += 1;
continue;
}
if (char === '"' || char === '\'' || char === '`') {
if (depth === 0 && expectingKey) {
const closeIndex = findStringEnd(objectText, index, char);
if (closeIndex > index) {
const key = readOptionalString(objectText.slice(index + 1, closeIndex));
const afterKey = skipWhitespace(objectText, closeIndex + 1);
if (key && objectText[afterKey] === ':') {
keys.push(key);
expectingKey = false;
index = afterKey + 1;
continue;
}
}
}
quote = char;
index += 1;
continue;
}
if (depth === 0 && expectingKey && /[A-Za-z_$]/.test(char)) {
let endIndex = index + 1;
while (endIndex < objectText.length && /[\w$-]/.test(objectText[endIndex] ?? '')) {
endIndex += 1;
}
const key = objectText.slice(index, endIndex);
const afterKey = skipWhitespace(objectText, endIndex);
if (objectText[afterKey] === ':') {
keys.push(key);
expectingKey = false;
index = afterKey + 1;
continue;
}
}
if (char === '{' || char === '[' || char === '(') {
depth += 1;
} else if (char === '}' || char === ']' || char === ')') {
depth = Math.max(0, depth - 1);
} else if (char === ',' && depth === 0) {
expectingKey = true;
}
index += 1;
}
return [...new Set(keys)].sort((left, right) => left.localeCompare(right));
}
function skipWhitespace(source: string, startIndex: number): number {
let index = startIndex;
while (index < source.length && /\s/.test(source[index] ?? '')) {
index += 1;
}
return index;
}
function findStringEnd(source: string, startIndex: number, quote: '"' | '\'' | '`'): number {
let escaped = false;
for (let index = startIndex + 1; index < source.length; index += 1) {
const char = source[index] ?? '';
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === quote) {
return index;
}
}
return -1;
}
function extractClassStaticsFromSource(source: string): readonly IClassStaticMetadata[] {
const classes: IClassStaticMetadata[] = [];
const classPattern = /(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\b/g;
let match: RegExpExecArray | null;
while ((match = classPattern.exec(source)) !== null) {
const className = match[1] ?? '';
const openIndex = source.indexOf('{', match.index + match[0].length);
if (!className || openIndex < 0) {
continue;
}
const closeIndex = findMatchingBrace(source, openIndex);
if (closeIndex < 0) {
continue;
}
const classBody = source.slice(openIndex + 1, closeIndex);
classes.push({
className,
...(readStaticString(classBody, 'description') ? { description: readStaticString(classBody, 'description') } : {}),
...(readStaticString(classBody, 'kind') ? { kind: readStaticString(classBody, 'kind') } : {}),
accepts: readStaticObjectKeys(classBody, 'accepts'),
emits: readStaticObjectKeys(classBody, 'emits'),
skills: readStaticObjectKeys(classBody, 'skills'),
tags: readStaticArray(classBody, 'tags'),
capabilities: readStaticArray(classBody, 'capabilities'),
});
classPattern.lastIndex = closeIndex + 1;
}
return classes;
}
function extractClassStatics(packageDir: string): ReadonlyMap<string, IClassStaticMetadata> {
const statics = new Map<string, IClassStaticMetadata>();
for (const sourceFile of readSourceFiles(packageDir)) {
const source = fs.readFileSync(sourceFile, 'utf8');
for (const entry of extractClassStaticsFromSource(source)) {
statics.set(entry.className, entry);
}
}
return statics;
}
export function extractDiscoveryMetadata(packageDir: string): IPackageDiscoveryMetadata {
const resolved = readManifestFromPackageDir(packageDir);
const version = readVersionFromPackageDir(packageDir);
const manifest = resolved.manifest;
const classStatics = extractClassStatics(packageDir);
const componentEntries = Array.isArray(manifest.components)
? manifest.components
.map(readComponentManifestEntry)
.filter((entry): entry is IComponentDiscoveryMetadata => Boolean(entry))
: [];
const webapp = readWebappMetadata(manifest);
const declaredKind = readOptionalString(manifest.declaredKind)
?? readOptionalString(manifest.kind)
?? readOptionalString(manifest.class);
const displayName = readOptionalString(manifest.displayName)
?? webapp?.displayName
?? resolved.packageName.split('/').at(-1);
return {
packageName: resolved.packageName,
version,
...(displayName ? { displayName } : {}),
...(readPackageDescription(packageDir, manifest) ? { description: readPackageDescription(packageDir, manifest) } : {}),
...(declaredKind ? { declaredKind } : {}),
inferredKind: inferPackageKind(manifest),
tags: readStringArray(manifest.tags),
capabilities: readStringArray(manifest.capabilities),
permissions: asRecord(manifest.permissions),
...(webapp ? { webapp } : {}),
components: componentEntries.map((component) => enrichComponent(component, classStatics)),
};
}

View File

@ -0,0 +1,378 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import type {
IComponentDiscoveryMetadata,
IPackageDiscoveryMetadata,
} from './discovery-extractor.js';
type JsonRecord = Record<string, unknown>;
export interface IPackageDiscoveryIndexVersion {
readonly indexedAt: string;
readonly indexedBy: string;
readonly metadata: IPackageDiscoveryMetadata;
}
export interface IPackageDiscoveryIndexPackage {
readonly versions: Record<string, IPackageDiscoveryIndexVersion>;
}
export interface IPackageDiscoveryIndex {
readonly packages: Record<string, IPackageDiscoveryIndexPackage>;
}
export interface IPackageDiscoverySearchQuery {
readonly text?: string;
readonly tag?: string;
readonly inferredKind?: string;
readonly declaredKind?: string;
readonly capability?: string;
readonly accepts?: string;
readonly emits?: string;
readonly skill?: string;
readonly shells?: string;
}
export interface IPackageDiscoverySearchResult {
readonly packageName: string;
readonly version: string;
readonly description?: string;
readonly displayName?: string;
readonly inferredKind: string;
readonly declaredKind?: string;
readonly keywords: readonly string[];
readonly matchedFields: readonly string[];
readonly score: number;
readonly webapp?: {
readonly appName: string;
readonly routePrefix?: string;
readonly shells: readonly string[];
readonly icon?: string;
};
readonly components: readonly IComponentDiscoveryMetadata[];
}
function asRecord(value: unknown): JsonRecord | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as JsonRecord
: null;
}
function readOptionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function readStringArray(value: unknown): readonly string[] {
return Array.isArray(value)
? value
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)
: [];
}
function readComponent(value: unknown): IComponentDiscoveryMetadata | null {
const record = asRecord(value);
const type = readOptionalString(record?.type);
if (!record || !type) {
return null;
}
return {
type,
...(readOptionalString(record.exportName) ? { exportName: readOptionalString(record.exportName) } : {}),
...(readOptionalString(record.mount) ? { mount: readOptionalString(record.mount) } : {}),
...(readOptionalString(record.surface) ? { surface: readOptionalString(record.surface) } : {}),
...(readOptionalString(record.description) ? { description: readOptionalString(record.description) } : {}),
...(readOptionalString(record.kind) ? { kind: readOptionalString(record.kind) } : {}),
accepts: readStringArray(record.accepts),
emits: readStringArray(record.emits),
skills: readStringArray(record.skills),
tags: readStringArray(record.tags),
capabilities: readStringArray(record.capabilities),
};
}
function readPackageMetadata(value: unknown): IPackageDiscoveryMetadata | null {
const record = asRecord(value);
const packageName = readOptionalString(record?.packageName);
const version = readOptionalString(record?.version);
const inferredKind = readOptionalString(record?.inferredKind);
if (!record || !packageName || !version || !inferredKind) {
return null;
}
const webappRecord = asRecord(record.webapp);
const appName = readOptionalString(webappRecord?.appName);
return {
packageName,
version,
...(readOptionalString(record.displayName) ? { displayName: readOptionalString(record.displayName) } : {}),
...(readOptionalString(record.description) ? { description: readOptionalString(record.description) } : {}),
...(readOptionalString(record.declaredKind) ? { declaredKind: readOptionalString(record.declaredKind) } : {}),
inferredKind,
tags: readStringArray(record.tags),
capabilities: readStringArray(record.capabilities),
permissions: asRecord(record.permissions),
...(webappRecord && appName ? {
webapp: {
appName,
...(readOptionalString(webappRecord.routePrefix) ? { routePrefix: readOptionalString(webappRecord.routePrefix) } : {}),
...(readOptionalString(webappRecord.displayName) ? { displayName: readOptionalString(webappRecord.displayName) } : {}),
...(readOptionalString(webappRecord.icon) ? { icon: readOptionalString(webappRecord.icon) } : {}),
...(typeof webappRecord.navOrder === 'number' && Number.isFinite(webappRecord.navOrder)
? { navOrder: webappRecord.navOrder }
: {}),
...(readOptionalString(webappRecord.description) ? { description: readOptionalString(webappRecord.description) } : {}),
shells: readStringArray(webappRecord.shells),
},
} : {}),
components: Array.isArray(record.components)
? record.components
.map(readComponent)
.filter((entry): entry is IComponentDiscoveryMetadata => Boolean(entry))
: [],
};
}
function resolveDiscoveryIndexPath(registryRoot: string): string {
return path.join(registryRoot, 'discovery-index.json');
}
export function readPackageDiscoveryIndex(registryRoot: string): IPackageDiscoveryIndex {
const indexPath = resolveDiscoveryIndexPath(registryRoot);
if (!fs.existsSync(indexPath)) {
return { packages: {} };
}
const root = asRecord(JSON.parse(fs.readFileSync(indexPath, 'utf8')) as unknown);
const packagesRecord = asRecord(root?.packages) ?? {};
const packages: Record<string, IPackageDiscoveryIndexPackage> = {};
for (const [packageName, packageValue] of Object.entries(packagesRecord)) {
const packageRecord = asRecord(packageValue);
const versionsRecord = asRecord(packageRecord?.versions) ?? {};
const versions: Record<string, IPackageDiscoveryIndexVersion> = {};
for (const [version, versionValue] of Object.entries(versionsRecord)) {
const versionRecord = asRecord(versionValue);
const indexedAt = readOptionalString(versionRecord?.indexedAt);
const indexedBy = readOptionalString(versionRecord?.indexedBy);
const metadata = readPackageMetadata(versionRecord?.metadata);
if (!indexedAt || !indexedBy || !metadata) {
continue;
}
versions[version] = { indexedAt, indexedBy, metadata };
}
packages[packageName] = { versions };
}
return { packages };
}
export function publishPackageDiscoveryMetadata(
registryRoot: string,
metadata: IPackageDiscoveryMetadata,
indexedBy: string,
): void {
const index = readPackageDiscoveryIndex(registryRoot);
const packageEntry = index.packages[metadata.packageName] ?? { versions: {} };
packageEntry.versions[metadata.version] = {
indexedAt: new Date().toISOString(),
indexedBy,
metadata,
};
index.packages[metadata.packageName] = packageEntry;
const indexPath = resolveDiscoveryIndexPath(registryRoot);
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
}
function latestVersionEntry(entry: IPackageDiscoveryIndexPackage): IPackageDiscoveryIndexVersion | null {
const versions = Object.entries(entry.versions);
if (versions.length === 0) {
return null;
}
versions.sort(([left], [right]) => left.localeCompare(right, undefined, { numeric: true }));
return versions.at(-1)?.[1] ?? null;
}
function toLowerTerms(values: readonly string[]): readonly string[] {
return values
.map((value) => value.trim().toLowerCase())
.filter((value) => value.length > 0);
}
function globMatches(pattern: string | undefined, values: readonly string[]): boolean {
if (!pattern) {
return true;
}
const lowerPattern = pattern.toLowerCase();
const escaped = lowerPattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
const regexp = new RegExp(`^${escaped}$`);
return values.some((value) => regexp.test(value.toLowerCase()));
}
function collectSearchFields(metadata: IPackageDiscoveryMetadata): readonly [string, string][] {
const fields: Array<[string, string]> = [];
function push(field: string, value: string | undefined): void {
if (value && value.trim().length > 0) {
fields.push([field, value]);
}
}
push('packageName', metadata.packageName);
push('displayName', metadata.displayName);
push('description', metadata.description);
push('inferredKind', metadata.inferredKind);
push('declaredKind', metadata.declaredKind);
for (const tag of metadata.tags) push('tag', tag);
for (const capability of metadata.capabilities) push('capability', capability);
if (metadata.webapp) {
push('webapp.appName', metadata.webapp.appName);
push('webapp.displayName', metadata.webapp.displayName);
push('webapp.description', metadata.webapp.description);
for (const shell of metadata.webapp.shells) push('webapp.shells', shell);
}
for (const component of metadata.components) {
push('component.type', component.type);
push('component.exportName', component.exportName);
push('component.mount', component.mount);
push('component.surface', component.surface);
push('component.description', component.description);
push('component.kind', component.kind);
for (const value of component.accepts) push('component.accepts', value);
for (const value of component.emits) push('component.emits', value);
for (const value of component.skills) push('component.skills', value);
for (const value of component.tags) push('component.tags', value);
for (const value of component.capabilities) push('component.capabilities', value);
}
return fields;
}
function scoreText(metadata: IPackageDiscoveryMetadata, text: string | undefined): {
readonly matches: boolean;
readonly score: number;
readonly matchedFields: readonly string[];
} {
const tokens = toLowerTerms((text ?? '').split(/\s+/));
if (tokens.length === 0) {
return { matches: true, score: 0, matchedFields: [] };
}
const fields = collectSearchFields(metadata);
let score = 0;
const matchedFields = new Set<string>();
for (const token of tokens) {
let tokenMatched = false;
for (const [field, value] of fields) {
if (value.toLowerCase().includes(token)) {
tokenMatched = true;
matchedFields.add(field);
score += field === 'packageName' || field === 'displayName' ? 5 : 1;
}
}
if (!tokenMatched) {
return { matches: false, score: 0, matchedFields: [] };
}
}
return {
matches: true,
score,
matchedFields: [...matchedFields].sort((left, right) => left.localeCompare(right)),
};
}
function metadataMatchesFilters(metadata: IPackageDiscoveryMetadata, query: IPackageDiscoverySearchQuery): boolean {
if (query.tag && !metadata.tags.some((tag) => tag.toLowerCase() === query.tag?.toLowerCase())) {
return false;
}
if (query.inferredKind && metadata.inferredKind.toLowerCase() !== query.inferredKind.toLowerCase()) {
return false;
}
if (query.declaredKind && metadata.declaredKind?.toLowerCase() !== query.declaredKind.toLowerCase()) {
return false;
}
if (query.capability && !metadata.capabilities.some((capability) => capability.toLowerCase() === query.capability?.toLowerCase())) {
return false;
}
if (query.shells && !metadata.webapp?.shells.some((shell) => shell.toLowerCase() === query.shells?.toLowerCase())) {
return false;
}
if (!globMatches(query.accepts, metadata.components.flatMap((component) => component.accepts))) {
return false;
}
if (!globMatches(query.emits, metadata.components.flatMap((component) => component.emits))) {
return false;
}
if (!globMatches(query.skill, metadata.components.flatMap((component) => component.skills))) {
return false;
}
return true;
}
function keywordsFor(metadata: IPackageDiscoveryMetadata): readonly string[] {
return [
metadata.inferredKind,
...(metadata.declaredKind ? [metadata.declaredKind] : []),
...metadata.tags,
...metadata.capabilities,
...(metadata.webapp ? metadata.webapp.shells : []),
...metadata.components.flatMap((component) => [
component.surface,
component.kind,
...component.skills,
...component.tags,
...component.capabilities,
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0)),
].filter((value, index, all) => all.indexOf(value) === index);
}
export function searchPackageDiscoveryIndex(
registryRoot: string,
query: IPackageDiscoverySearchQuery,
limit: number,
): readonly IPackageDiscoverySearchResult[] {
const index = readPackageDiscoveryIndex(registryRoot);
const results: IPackageDiscoverySearchResult[] = [];
for (const [packageName, entry] of Object.entries(index.packages)) {
const latest = latestVersionEntry(entry);
if (!latest) {
continue;
}
const metadata = latest.metadata;
if (metadata.packageName !== packageName) {
continue;
}
if (!metadataMatchesFilters(metadata, query)) {
continue;
}
const textScore = scoreText(metadata, query.text);
if (!textScore.matches) {
continue;
}
results.push({
packageName: metadata.packageName,
version: metadata.version,
...(metadata.description ? { description: metadata.description } : {}),
...(metadata.displayName ? { displayName: metadata.displayName } : {}),
inferredKind: metadata.inferredKind,
...(metadata.declaredKind ? { declaredKind: metadata.declaredKind } : {}),
keywords: keywordsFor(metadata),
matchedFields: textScore.matchedFields,
score: textScore.score,
...(metadata.webapp ? {
webapp: {
appName: metadata.webapp.appName,
...(metadata.webapp.routePrefix ? { routePrefix: metadata.webapp.routePrefix } : {}),
shells: metadata.webapp.shells,
...(metadata.webapp.icon ? { icon: metadata.webapp.icon } : {}),
},
} : {}),
components: metadata.components,
});
}
return results
.sort((left, right) => {
if (left.score !== right.score) return right.score - left.score;
return left.packageName.localeCompare(right.packageName);
})
.slice(0, limit);
}

View File

@ -0,0 +1,255 @@
export interface IManifestDiagnostic {
readonly severity: 'error' | 'warn';
readonly code: string;
readonly message: string;
readonly field?: string;
readonly componentIndex?: number;
}
export interface IManifestValidationResult {
readonly valid: boolean;
readonly errors: string[];
readonly diagnostics: ReadonlyArray<IManifestDiagnostic>;
}
type JsonRecord = Record<string, unknown>;
const SUPPORTED_FS_POLICIES = new Set([
'none',
'matrix-only',
'project-readonly',
'project-readwrite',
'global-readonly',
]);
const SUPPORTED_HOST_API_POLICIES = new Set([
'none',
'same-origin',
]);
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function optionalTrimmedString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function validateInstallHook(
diagnostics: IManifestDiagnostic[],
install: JsonRecord,
phase: 'validate' | 'migrate' | 'seed' | 'verify',
): void {
const hook = asRecord(install[phase]);
if (!hook) {
if (install[phase] !== undefined) {
pushError(diagnostics, {
code: 'MXPKG_INSTALL_HOOK_INVALID',
field: `install.${phase}`,
message: `install.${phase} must be an object when provided`,
});
}
return;
}
if (!optionalTrimmedString(hook.script)) {
pushError(diagnostics, {
code: 'MXPKG_INSTALL_HOOK_SCRIPT_INVALID',
field: `install.${phase}.script`,
message: `install.${phase}.script must be a non-empty string`,
});
}
}
function pushError(
diagnostics: IManifestDiagnostic[],
entry: Omit<IManifestDiagnostic, 'severity'>
): void {
diagnostics.push({ ...entry, severity: 'error' });
}
export function validateManifestForMxCli(manifest: unknown): IManifestValidationResult {
const diagnostics: IManifestDiagnostic[] = [];
const manifestRecord = asRecord(manifest);
if (!manifestRecord) {
pushError(diagnostics, {
code: 'MXPKG_MANIFEST_NOT_OBJECT',
message: 'Manifest must be a JSON object',
});
return {
valid: false,
diagnostics,
errors: diagnostics.map((entry) => entry.message),
};
}
if (!optionalTrimmedString(manifestRecord.name)) {
pushError(diagnostics, {
code: 'MXPKG_NAME_INVALID',
field: 'name',
message: 'name must be a non-empty string',
});
}
if (!optionalTrimmedString(manifestRecord.version)) {
pushError(diagnostics, {
code: 'MXPKG_VERSION_INVALID',
field: 'version',
message: 'version must be a non-empty string',
});
}
const runtime = asRecord(manifestRecord.runtime);
if (!runtime || !optionalTrimmedString(runtime.language) || !optionalTrimmedString(runtime.entry)) {
pushError(diagnostics, {
code: 'MXPKG_RUNTIME_INVALID',
field: 'runtime',
message: 'runtime.language and runtime.entry are required',
});
}
const permissions = asRecord(manifestRecord.permissions);
const fsPolicy = permissions ? optionalTrimmedString(permissions.fsPolicy) : undefined;
if (!fsPolicy || !SUPPORTED_FS_POLICIES.has(fsPolicy)) {
pushError(diagnostics, {
code: 'MXPKG_PERMISSIONS_FSPOLICY_INVALID',
field: 'permissions.fsPolicy',
message: 'permissions.fsPolicy must be one of none|matrix-only|project-readonly|project-readwrite|global-readonly',
});
}
const hostApi = permissions ? optionalTrimmedString(permissions.hostApi) : undefined;
if (permissions?.hostApi !== undefined && (!hostApi || !SUPPORTED_HOST_API_POLICIES.has(hostApi))) {
pushError(diagnostics, {
code: 'MXPKG_PERMISSIONS_HOSTAPI_INVALID',
field: 'permissions.hostApi',
message: 'permissions.hostApi must be one of none|same-origin',
});
}
for (const field of ['network', 'subprocess', 'env']) {
if (permissions && permissions[field] !== undefined && typeof permissions[field] !== 'boolean') {
pushError(diagnostics, {
code: 'MXPKG_PERMISSIONS_BOOLEAN_INVALID',
field: `permissions.${field}`,
message: `permissions.${field} must be boolean when provided`,
});
}
}
const install = asRecord(manifestRecord.install);
if (manifestRecord.install !== undefined && !install) {
pushError(diagnostics, {
code: 'MXPKG_INSTALL_INVALID',
field: 'install',
message: 'install must be an object when provided',
});
} else if (install) {
validateInstallHook(diagnostics, install, 'validate');
validateInstallHook(diagnostics, install, 'migrate');
validateInstallHook(diagnostics, install, 'seed');
validateInstallHook(diagnostics, install, 'verify');
}
const componentsValue = manifestRecord.components;
if (!Array.isArray(componentsValue)) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENTS_NOT_ARRAY',
field: 'components',
message: 'components must be an array',
});
} else {
componentsValue.forEach((entry, componentIndex) => {
const component = asRecord(entry);
if (!component) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_NOT_OBJECT',
field: 'components',
componentIndex,
message: `component[${componentIndex}] must be an object`,
});
return;
}
const type = optionalTrimmedString(component.type);
if (!type) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_TYPE_INVALID',
field: 'type',
componentIndex,
message: `component[${componentIndex}].type must be a non-empty string`,
});
return;
}
const exportName = optionalTrimmedString(component.export);
if (component.export !== undefined && !exportName) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_EXPORT_INVALID',
field: 'export',
componentIndex,
message: `component[${componentIndex}].export must be a non-empty string when provided`,
});
return;
}
const mount = optionalTrimmedString(component.mount);
if (component.mount !== undefined && !mount) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_MOUNT_INVALID',
field: 'mount',
componentIndex,
message: `component[${componentIndex}].mount must be a non-empty string when provided`,
});
return;
}
if (component.autoStart !== undefined && typeof component.autoStart !== 'boolean') {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_AUTOSTART_INVALID',
field: 'autoStart',
componentIndex,
message: `component[${componentIndex}].autoStart must be boolean when provided`,
});
return;
}
if (component.props !== undefined && !asRecord(component.props)) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_PROPS_INVALID',
field: 'props',
componentIndex,
message: `component[${componentIndex}].props must be an object when provided`,
});
return;
}
const componentFsPolicy = optionalTrimmedString(component.fsPolicy);
if (
component.fsPolicy !== undefined
&& (!componentFsPolicy || !SUPPORTED_FS_POLICIES.has(componentFsPolicy))
) {
pushError(diagnostics, {
code: 'MXPKG_COMPONENT_FSPOLICY_INVALID',
field: 'fsPolicy',
componentIndex,
message: `component[${componentIndex}].fsPolicy must be one of matrix-only|project-readonly|project-readwrite|global-readonly|none`,
});
}
});
}
const errors = diagnostics
.filter((entry) => entry.severity === 'error')
.map((entry) => entry.message);
return {
valid: errors.length === 0,
diagnostics,
errors,
};
}

View File

@ -0,0 +1,345 @@
import { spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { readRegistryCredential } from './auth-store.js';
export const MATRIX_NPM_REGISTRY_SCOPES = ['@open-matrix', '@matrix', '@omega'] as const;
export interface INpmCommandResult {
readonly exitCode: number | null;
readonly stdout: string;
readonly stderr: string;
}
export interface INpmRegistrySearchResult {
readonly packageName: string;
readonly version: string;
readonly description?: string;
readonly keywords: readonly string[];
}
export function isHttpRegistryUrl(value: string | undefined): value is string {
if (!value) {
return false;
}
try {
const parsed = new URL(value);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
export function npmPackageSpecifier(packageName: string, version: string | undefined): string {
return version ? `${packageName}@${version}` : packageName;
}
export async function prepareNpmRegistryTarball(
specifier: string,
registryUrl: string,
cwd: string,
credentialsPath: string | undefined,
): Promise<{ readonly tarballPath: string; readonly cleanupDir: string }> {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-install-npm-registry-'));
const credential = readRegistryCredential(cwd, {
registry: registryUrl,
credentialsPath,
}).credential;
const userConfig = writeTemporaryNpmrc(tempDir, registryUrl, credential?.token ?? null);
const result = await runNpmCommand([
'pack',
specifier,
'--pack-destination',
tempDir,
'--registry',
registryUrl,
'--userconfig',
userConfig,
], {
cwd: tempDir,
registryUrl,
userConfig,
cacheDir: path.join(tempDir, '.npm-cache'),
});
if (result.exitCode !== 0) {
fs.rmSync(tempDir, { recursive: true, force: true });
const output = [result.stderr, result.stdout]
.filter((value) => typeof value === 'string' && value.trim().length > 0)
.join('\n')
.trim();
throw new Error(
`MX_REGISTRY_FETCH_FAILED: failed to fetch ${specifier} from ${registryUrl}`
+ (output ? `\n${output}` : ''),
);
}
const tarballs = fs.readdirSync(tempDir)
.filter((entry) => entry.endsWith('.tgz') || entry.endsWith('.tar.gz'));
if (tarballs.length === 0) {
fs.rmSync(tempDir, { recursive: true, force: true });
throw new Error(`MX_REGISTRY_FETCH_FAILED: npm pack produced no tarball for ${specifier}`);
}
return {
tarballPath: path.join(tempDir, tarballs[0]!),
cleanupDir: tempDir,
};
}
export async function packNpmPublishTarball(
packageDir: string,
outDir: string,
): Promise<{ readonly tarballPath: string; readonly stdout: string; readonly stderr: string }> {
fs.mkdirSync(outDir, { recursive: true });
const result = await runNpmCommand([
'pack',
packageDir,
'--pack-destination',
outDir,
], {
cwd: packageDir,
cacheDir: path.join(outDir, '.npm-cache'),
});
if (result.exitCode !== 0) {
const output = [result.stderr, result.stdout]
.filter((value) => typeof value === 'string' && value.trim().length > 0)
.join('\n')
.trim();
throw new Error(`MX_REGISTRY_PACK_FAILED: npm pack failed${output ? `\n${output}` : ''}`);
}
const stdoutTarball = result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.reverse()
.find((line) => line.endsWith('.tgz') || line.endsWith('.tar.gz'));
if (stdoutTarball) {
const tarballPath = path.join(outDir, path.basename(stdoutTarball));
if (fs.existsSync(tarballPath)) {
return {
tarballPath,
stdout: result.stdout,
stderr: result.stderr,
};
}
}
const tarball = fs.readdirSync(outDir)
.filter((entry) => entry.endsWith('.tgz') || entry.endsWith('.tar.gz'))
.map((entry) => ({
entry,
mtimeMs: fs.statSync(path.join(outDir, entry)).mtimeMs,
}))
.sort((left, right) => left.mtimeMs - right.mtimeMs)
.at(-1);
if (!tarball) {
throw new Error(`MX_REGISTRY_PACK_FAILED: npm pack produced no tarball under ${outDir}`);
}
return {
tarballPath: path.join(outDir, tarball.entry),
stdout: result.stdout,
stderr: result.stderr,
};
}
export async function publishNpmTarball(
tarballPath: string,
registryUrl: string,
cwd: string,
credentialsPath: string | undefined,
): Promise<INpmCommandResult> {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-publish-npm-registry-'));
try {
const credential = readRegistryCredential(cwd, {
registry: registryUrl,
credentialsPath,
}).credential;
const userConfig = writeTemporaryNpmrc(tempDir, registryUrl, credential?.token ?? null);
const result = await runNpmCommand([
'publish',
tarballPath,
'--registry',
registryUrl,
'--userconfig',
userConfig,
], {
cwd,
registryUrl,
userConfig,
cacheDir: path.join(tempDir, '.npm-cache'),
});
return result;
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
export async function searchNpmRegistry(
query: string,
registryUrl: string,
cwd: string,
credentialsPath: string | undefined,
limit: number,
): Promise<readonly INpmRegistrySearchResult[]> {
const trimmedQuery = query.trim();
if (!trimmedQuery) {
throw new Error('MX_SEARCH_INVALID: query is required');
}
const searchUrl = new URL('-/v1/search', registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`);
searchUrl.searchParams.set('text', trimmedQuery);
searchUrl.searchParams.set('size', String(limit));
const credential = readRegistryCredential(cwd, {
registry: registryUrl,
credentialsPath,
}).credential;
const response = await fetch(searchUrl, {
headers: {
accept: 'application/json',
...(credential?.token ? { authorization: `Bearer ${credential.token}` } : {}),
},
});
if (!response.ok) {
const body = await response.text();
throw new Error(
`MX_REGISTRY_SEARCH_FAILED: registry search failed with HTTP ${response.status}`
+ (body.trim() ? `\n${body.trim()}` : ''),
);
}
const parsed = asRecord(await response.json() as unknown);
const objects = Array.isArray(parsed?.objects) ? parsed.objects : [];
const results: INpmRegistrySearchResult[] = [];
for (const object of objects) {
const packageRecord = asRecord(asRecord(object)?.package);
const packageName = readNpmSearchPackageName(packageRecord);
const version = readString(packageRecord?.version);
if (!packageName || !version) {
continue;
}
results.push({
packageName,
version,
...(readString(packageRecord?.description) ? { description: readString(packageRecord?.description)! } : {}),
keywords: readStringArray(packageRecord?.keywords),
});
}
return results;
}
function readNpmSearchPackageName(packageRecord: Record<string, unknown> | null): string | undefined {
const name = readString(packageRecord?.name);
if (!name) {
return undefined;
}
if (name.startsWith('@')) {
return name;
}
const scope = readString(packageRecord?.scope);
if (!scope) {
return name;
}
return `${scope.startsWith('@') ? scope : `@${scope}`}/${name}`;
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function readString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function readStringArray(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
return value
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
}
function registryAuthConfigKey(registryUrl: string): string {
const parsed = new URL(registryUrl);
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
return `//${parsed.host}${pathname}:_authToken`;
}
function writeTemporaryNpmrc(
tempDir: string,
registryUrl: string,
token: string | null,
): string {
const npmrcPath = path.join(tempDir, '.npmrc');
const lines = [
`registry=${registryUrl}`,
];
for (const scope of MATRIX_NPM_REGISTRY_SCOPES) {
lines.push(`${scope}:registry=${registryUrl}`);
}
if (token) {
lines.push('always-auth=true');
lines.push(`${registryAuthConfigKey(registryUrl)}=${token}`);
}
fs.writeFileSync(npmrcPath, `${lines.join('\n')}\n`, 'utf8');
return npmrcPath;
}
async function runNpmCommand(
args: readonly string[],
options: {
readonly cwd: string;
readonly registryUrl?: string;
readonly userConfig?: string;
readonly cacheDir: string;
},
): Promise<INpmCommandResult> {
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
fs.mkdirSync(options.cacheDir, { recursive: true });
const globalConfig = path.join(options.cacheDir, 'empty-global-npmrc');
if (!fs.existsSync(globalConfig)) {
fs.writeFileSync(globalConfig, '', 'utf8');
}
return await new Promise((resolve, reject) => {
const child = spawn(npmCommand, [...args], {
cwd: options.cwd,
env: {
...process.env,
...(options.userConfig ? {
NPM_CONFIG_USERCONFIG: options.userConfig,
npm_config_userconfig: options.userConfig,
} : {}),
NPM_CONFIG_GLOBALCONFIG: globalConfig,
npm_config_globalconfig: globalConfig,
...(options.registryUrl ? { npm_config_registry: options.registryUrl } : {}),
npm_config_cache: options.cacheDir,
},
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
const stdout: string[] = [];
const stderr: string[] = [];
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => stdout.push(chunk));
child.stderr.on('data', (chunk: string) => stderr.push(chunk));
child.on('error', reject);
child.on('close', (exitCode) => {
resolve({
exitCode,
stdout: stdout.join(''),
stderr: stderr.join(''),
});
});
});
}

View File

@ -0,0 +1,212 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { validateManifestForMxCli } from './manifestValidator.js';
type JsonRecord = Record<string, unknown>;
export interface IResolvedManifest {
readonly packageName: string;
readonly manifest: JsonRecord;
}
const DEFAULT_GLOBAL_PACKAGES_ROOT = path.join(os.homedir(), '.matrix', 'packages');
const DEFAULT_GLOBAL_REGISTRY_ROOT = path.join(os.homedir(), '.matrix', 'registry');
export function resolvePackagesRoot(cwd: string, packagesDir?: string): string {
return path.resolve(cwd, packagesDir ?? DEFAULT_GLOBAL_PACKAGES_ROOT);
}
export function resolvePackagesNodeModulesRoot(cwd: string, packagesDir?: string): string {
return path.join(resolvePackagesRoot(cwd, packagesDir), 'node_modules');
}
export function resolveRegistryRoot(cwd: string, registryDir?: string): string {
return path.resolve(cwd, registryDir ?? DEFAULT_GLOBAL_REGISTRY_ROOT);
}
export function packageNameToPath(packageName: string): string {
if (packageName.includes('/')) {
return packageName;
}
return packageName.trim();
}
export function listInstalledPackageNames(nodeModulesRoot: string): string[] {
if (!fs.existsSync(nodeModulesRoot)) {
return [];
}
const entries = fs.readdirSync(nodeModulesRoot, { withFileTypes: true });
const names: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.')) continue;
if (entry.name.startsWith('@')) {
const scopeRoot = path.join(nodeModulesRoot, entry.name);
for (const scoped of fs.readdirSync(scopeRoot, { withFileTypes: true })) {
if (!scoped.isDirectory()) continue;
names.push(`${entry.name}/${scoped.name}`);
}
continue;
}
names.push(entry.name);
}
return names.sort((a, b) => a.localeCompare(b));
}
function readJsonFile(filePath: string): JsonRecord {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${filePath} must contain a JSON object`);
}
return parsed as JsonRecord;
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function readOptionalString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function readPackageJsonIfExists(packageDir: string): JsonRecord | null {
const packageJsonPath = path.join(packageDir, 'package.json');
return fs.existsSync(packageJsonPath) ? readJsonFile(packageJsonPath) : null;
}
function packageNameComparable(value: string): string {
const trimmed = value.trim();
if (trimmed.startsWith('@matrix/')) {
return trimmed.slice('@matrix/'.length);
}
if (trimmed.startsWith('@open-matrix/')) {
return trimmed.slice('@open-matrix/'.length);
}
if (trimmed.startsWith('@') && trimmed.includes('/')) {
return trimmed.slice(trimmed.indexOf('/') + 1);
}
return trimmed;
}
function assertMatrixAndPackageJsonAgree(
packageDir: string,
matrixManifest: JsonRecord,
packageJson: JsonRecord | null,
): void {
if (!packageJson) {
return;
}
const matrixName = readOptionalString(matrixManifest.name);
const packageName = readOptionalString(packageJson.name);
if (
matrixName
&& packageName
&& matrixName !== packageName
&& packageNameComparable(matrixName) !== packageNameComparable(packageName)
) {
throw new Error(
`MX_PACKAGE_IDENTITY_MISMATCH: matrix.json name (${matrixName}) `
+ `does not match package.json name (${packageName}) in ${packageDir}`,
);
}
const matrixVersion = readOptionalString(matrixManifest.version);
const packageVersion = readOptionalString(packageJson.version);
if (matrixVersion && packageVersion && matrixVersion !== packageVersion) {
throw new Error(
`MX_PACKAGE_VERSION_MISMATCH: matrix.json version (${matrixVersion}) `
+ `does not match package.json version (${packageVersion}) in ${packageDir}`,
);
}
}
export function readManifestFromPackageDir(packageDir: string): IResolvedManifest {
const matrixJsonPath = path.join(packageDir, 'matrix.json');
if (fs.existsSync(matrixJsonPath)) {
const matrixManifest = readJsonFile(matrixJsonPath);
const packageJson = readPackageJsonIfExists(packageDir);
assertMatrixAndPackageJsonAgree(packageDir, matrixManifest, packageJson);
const packageName = readOptionalString(matrixManifest.name);
if (!packageName) {
throw new Error(`matrix.json must contain a non-empty "name" field (${matrixJsonPath})`);
}
return {
packageName,
manifest: matrixManifest,
};
}
const packageJsonPath = path.join(packageDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Package manifest not found in ${packageDir} (expected matrix.json or package.json)`);
}
const packageJson = readJsonFile(packageJsonPath);
const packageName = typeof packageJson.name === 'string' ? packageJson.name.trim() : '';
if (!packageName) {
throw new Error(`package.json must contain a non-empty "name" field (${packageJsonPath})`);
}
const matrix = asRecord(packageJson.matrix) ?? {};
const manifest: JsonRecord = {
name: packageName,
version: packageJson.version,
runtime: matrix.runtime,
components: matrix.components,
permissions: matrix.permissions,
install: matrix.install,
};
return { packageName, manifest };
}
export function validateResolvedManifest(packageName: string, manifest: JsonRecord): void {
const validation = validateManifestForMxCli({
...manifest,
name: manifest.name ?? packageName,
});
if (!validation.valid) {
throw new Error(
`Manifest validation failed for ${packageName}: ${validation.errors.join('; ')}`
);
}
}
export function readPackageJson(packageDir: string): JsonRecord {
const packageJsonPath = path.join(packageDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`package.json not found in ${packageDir}`);
}
return readJsonFile(packageJsonPath);
}
export function readVersionFromPackageDir(packageDir: string): string {
const matrixJsonPath = path.join(packageDir, 'matrix.json');
const packageJson = readPackageJsonIfExists(packageDir);
if (fs.existsSync(matrixJsonPath)) {
const matrixManifest = readJsonFile(matrixJsonPath);
assertMatrixAndPackageJsonAgree(packageDir, matrixManifest, packageJson);
const matrixVersion = readOptionalString(matrixManifest.version);
const packageVersion = readOptionalString(packageJson?.version);
if (packageVersion.length > 0) {
return packageVersion;
}
if (matrixVersion.length > 0) {
return matrixVersion;
}
}
const version = readOptionalString((packageJson ?? readPackageJson(packageDir)).version);
if (!version) {
throw new Error(`Unable to resolve package version in ${packageDir}`);
}
return version;
}

View File

@ -0,0 +1,131 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { packageNameToPath } from './package-store.js';
import { maxSemver } from './versioning.js';
type JsonRecord = Record<string, unknown>;
export interface IRegistryVersionEntry {
readonly tarballPath: string;
readonly publishedAt: string;
readonly publishedBy: string;
}
export interface IRegistryPackageEntry {
readonly versions: Record<string, IRegistryVersionEntry>;
}
export interface IRegistryIndex {
readonly packages: Record<string, IRegistryPackageEntry>;
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function readRegistryIndex(indexPath: string): IRegistryIndex {
if (!fs.existsSync(indexPath)) {
return { packages: {} };
}
const parsed = JSON.parse(fs.readFileSync(indexPath, 'utf8')) as unknown;
const root = asRecord(parsed);
const packagesRecord = asRecord(root?.packages) ?? {};
const packages: Record<string, IRegistryPackageEntry> = {};
for (const [packageName, packageValue] of Object.entries(packagesRecord)) {
const packageEntry = asRecord(packageValue);
const versionsRecord = asRecord(packageEntry?.versions) ?? {};
const versions: Record<string, IRegistryVersionEntry> = {};
for (const [version, versionValue] of Object.entries(versionsRecord)) {
const versionEntry = asRecord(versionValue);
if (!versionEntry) continue;
if (typeof versionEntry.tarballPath !== 'string') continue;
if (typeof versionEntry.publishedAt !== 'string') continue;
if (typeof versionEntry.publishedBy !== 'string') continue;
versions[version] = {
tarballPath: versionEntry.tarballPath,
publishedAt: versionEntry.publishedAt,
publishedBy: versionEntry.publishedBy,
};
}
packages[packageName] = { versions };
}
return { packages };
}
function writeRegistryIndex(indexPath: string, index: IRegistryIndex): void {
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
}
function resolveIndexPath(registryRoot: string): string {
return path.join(registryRoot, 'index.json');
}
export function publishRegistryVersion(
registryRoot: string,
packageName: string,
version: string,
sourceTarballPath: string,
publishedBy: string,
): { readonly registryTarballPath: string } {
const indexPath = resolveIndexPath(registryRoot);
const index = readRegistryIndex(indexPath);
const packageEntry = index.packages[packageName] ?? { versions: {} };
if (packageEntry.versions[version]) {
throw new Error(`MX_VERSION_EXISTS: ${packageName}@${version} already published`);
}
const packageVersionDir = path.join(registryRoot, 'packages', packageNameToPath(packageName), version);
fs.mkdirSync(packageVersionDir, { recursive: true });
const registryTarballPath = path.join(packageVersionDir, 'package.tar.gz');
fs.copyFileSync(sourceTarballPath, registryTarballPath);
packageEntry.versions[version] = {
tarballPath: registryTarballPath,
publishedAt: new Date().toISOString(),
publishedBy,
};
index.packages[packageName] = packageEntry;
writeRegistryIndex(indexPath, index);
return { registryTarballPath };
}
export function resolveRegistryTarball(
registryRoot: string,
packageName: string,
requestedVersion?: string,
): { readonly version: string; readonly tarballPath: string } {
const index = readRegistryIndex(resolveIndexPath(registryRoot));
const packageEntry = index.packages[packageName];
if (!packageEntry) {
throw new Error(`MX_PACKAGE_NOT_FOUND: ${packageName}`);
}
const version = requestedVersion ?? maxSemver(Object.keys(packageEntry.versions));
if (!version || !packageEntry.versions[version]) {
throw new Error(`MX_PACKAGE_NOT_FOUND: ${packageName}@${requestedVersion ?? 'latest'}`);
}
return {
version,
tarballPath: packageEntry.versions[version]!.tarballPath,
};
}
export function getLatestRegistryVersion(registryRoot: string, packageName: string): string | null {
const index = readRegistryIndex(resolveIndexPath(registryRoot));
const packageEntry = index.packages[packageName];
if (!packageEntry) {
return null;
}
return maxSemver(Object.keys(packageEntry.versions));
}

View File

@ -0,0 +1,16 @@
import {
DEFAULT_SECURITY_POLICY,
DEFAULT_SUPERVISION_POLICY,
SecurityRealm,
} from '@open-matrix/core';
import { createPolicyEngine } from '@open-matrix/core/core/security/PolicyPresets';
export function createRunnerSecurityRealm(): SecurityRealm {
return new SecurityRealm(
DEFAULT_SUPERVISION_POLICY,
DEFAULT_SECURITY_POLICY,
undefined,
undefined,
createPolicyEngine({ policyEngine: 'declarative', preset: 'standard' }),
);
}

View File

@ -0,0 +1,164 @@
/**
* Scaffolding utilities for mx CLI.
*
* Provides functions for creating package structures, templates, etc.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { validateManifestForMxCli } from './manifestValidator.js';
export interface ScaffoldOptions {
name: string;
description?: string;
version?: string;
}
/**
* Create a basic package structure.
*/
export function createPackageStructure(options: ScaffoldOptions): void {
const root = path.resolve(options.name);
const folders = ['src', 'examples', 'skills', 'tests'];
fs.mkdirSync(root, { recursive: true });
for (const folder of folders) {
fs.mkdirSync(path.join(root, folder), { recursive: true });
}
}
/**
* Generate matrix.json content.
*/
export function generateMatrixJson(options: ScaffoldOptions): string {
return JSON.stringify({
name: options.name,
version: options.version ?? '0.1.0',
description: options.description ?? '',
runtime: {
language: 'typescript',
entry: 'src/index.ts',
},
components: [],
permissions: {
fsPolicy: 'none',
},
}, null, 2);
}
export function toKebabCase(value: string): string {
return value
.trim()
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.toLowerCase();
}
export function toPascalCase(value: string): string {
return value
.trim()
.replace(/[_-]+/g, ' ')
.split(/\s+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}
export function writeJsonFile(filePath: string, value: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf8');
}
export function validateMatrixManifest(manifest: unknown): { valid: boolean; errors: string[] } {
const result = validateManifestForMxCli(manifest);
return { valid: result.valid, errors: result.errors };
}
/**
* Generate package.json for a scaffolded package.
*/
export function generatePackageJson(name: string, options?: {
webapp?: boolean;
service?: boolean;
scripts?: Record<string, string>;
}): Record<string, unknown> {
const scripts: Record<string, string> = options?.scripts ?? (options?.webapp
? { dev: 'vite', build: 'vite build', test: 'node --import tsx --test tests/**/*.spec.ts' }
: options?.service
? {
build: 'tsc -p tsconfig.json',
'build:watch': 'tsc -w -p tsconfig.json --preserveWatchOutput',
dev: 'tsc -w -p tsconfig.json --preserveWatchOutput',
test: 'node --import tsx --test tests/**/*.spec.ts',
}
: { build: 'tsc', test: 'node --import tsx --test tests/**/*.spec.ts' });
const dependencies: Record<string, string> = {};
const devDependencies: Record<string, string> = {
'typescript': '^5.0.0',
};
if (options?.service) {
dependencies['@open-matrix/core'] = '*';
} else {
devDependencies['@open-matrix/core'] = '*';
}
if (options?.webapp) {
devDependencies['@open-matrix/federation'] = '*';
devDependencies['vite'] = '^5.0.0';
devDependencies['nats'] = '^5.0.0';
}
return {
name: `@matrix/${name}`,
version: '0.1.0',
type: 'module',
files: [
'dist',
'matrix.json',
],
scripts,
...(Object.keys(dependencies).length > 0 ? { dependencies } : {}),
devDependencies,
};
}
/**
* Generate tsconfig.json for a scaffolded package.
*/
export function generateTsconfig(options?: { webapp?: boolean; service?: boolean }): Record<string, unknown> {
const compilerOptions: Record<string, unknown> = {
target: 'ES2022',
module: 'NodeNext',
moduleResolution: 'NodeNext',
outDir: 'dist',
rootDir: 'src',
declaration: true,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
};
if (options?.service) {
compilerOptions.incremental = true;
compilerOptions.tsBuildInfoFile = '.tsbuildinfo';
compilerOptions.sourceMap = true;
}
if (options?.webapp) {
compilerOptions.module = 'ESNext';
compilerOptions.moduleResolution = 'Bundler';
compilerOptions.noEmit = true;
compilerOptions.lib = ['ES2022', 'DOM', 'DOM.Iterable'];
delete compilerOptions.outDir;
delete compilerOptions.rootDir;
delete compilerOptions.declaration;
}
return {
compilerOptions,
include: ['src'],
};
}

View File

@ -0,0 +1,448 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
normalizeMountedInstanceSpec,
type IMountedInstanceMetadataInput,
type IMountedInstanceSecretRefInput,
} from '@open-matrix/core/runtime/MountedInstanceMetadata.js';
type JsonRecord = Record<string, unknown>;
export interface IMatrixServiceSecretRef extends IMountedInstanceSecretRefInput {}
export interface IMatrixServiceInstanceManifest extends IMountedInstanceMetadataInput {
readonly secretRefs?: ReadonlyArray<IMatrixServiceSecretRef>;
}
export interface IMatrixServiceBootstrapManifest {
readonly overrides?: Record<string, unknown>;
readonly root?: string;
readonly transport?: Record<string, unknown>;
readonly auth?: Record<string, unknown>;
readonly http?: Record<string, unknown>;
}
export interface IMatrixRuntimeManifest {
readonly language?: string;
readonly entry?: string;
readonly factory?: IMatrixServiceManifest;
}
export interface IMatrixServiceManifest {
readonly kind?: 'factory';
readonly entry?: string;
readonly export?: string;
/** Standalone-only bootstrap envelope. */
readonly bootstrap?: IMatrixServiceBootstrapManifest;
readonly overrides?: Record<string, unknown>;
readonly root?: string;
readonly mount?: string;
readonly config?: {
readonly file?: string;
};
readonly secrets?: {
readonly file?: string;
};
readonly transport?: Record<string, unknown>;
readonly auth?: Record<string, unknown>;
readonly http?: Record<string, unknown>;
readonly dependsOn?: ReadonlyArray<string>;
readonly policyRef?: string;
readonly instance?: IMatrixServiceInstanceManifest;
}
export interface IServiceInstanceBootstrap {
readonly id: string;
readonly packageName: string;
readonly packageRevision?: string;
readonly class: string;
readonly mount?: string;
readonly rootKind: string;
readonly componentType?: string;
readonly configRef?: string;
readonly secretRefs?: ReadonlyArray<{
readonly name: string;
readonly provider: string;
readonly ref: string;
readonly required?: boolean;
}>;
readonly dependsOn?: ReadonlyArray<string>;
readonly policyRef?: string;
readonly autoStart: boolean;
readonly registrationSource: 'matrix-service';
}
export interface ILoadedMatrixBindingDeclaration {
readonly type?: string;
readonly mount: string;
readonly accepts: readonly string[];
}
export interface ILoadedMatrixServiceManifest {
readonly packageDir: string;
readonly packageName: string;
readonly displayName?: string;
readonly packageNamespace?: string;
readonly manifestPath: string;
readonly entryPath: string;
readonly exportName: string;
readonly kind: 'factory';
readonly overrides: Record<string, unknown>;
readonly root?: string;
readonly mount?: string;
readonly configRef?: string;
readonly transport?: Record<string, unknown>;
readonly auth?: Record<string, unknown>;
readonly http?: Record<string, unknown>;
readonly packageRoot?: ILoadedMatrixBindingDeclaration;
readonly packageComponents: ReadonlyArray<ILoadedMatrixBindingDeclaration>;
readonly serviceInstance: IServiceInstanceBootstrap;
}
export function packageDeclaresStandaloneFactory(packageDir: string): boolean {
const resolvedPackageDir = path.resolve(packageDir);
const matrixManifestPath = path.join(resolvedPackageDir, 'matrix.json');
if (!fs.existsSync(matrixManifestPath)) {
return false;
}
const matrixManifest = readOptionalJsonObject(matrixManifestPath, 'matrix.json');
const runtime = asRecord(matrixManifest?.runtime);
return isRecord(runtime?.factory);
}
export function loadMatrixServiceManifest(packageDir: string): ILoadedMatrixServiceManifest {
const resolvedPackageDir = path.resolve(packageDir);
const matrixManifestPath = path.join(resolvedPackageDir, 'matrix.json');
const matrixManifest = readJsonObject(matrixManifestPath, 'matrix.json');
const packageName = optionalTrimmedString(matrixManifest.name) ?? path.basename(resolvedPackageDir);
const displayName = optionalTrimmedString(matrixManifest.displayName);
const packageVersion = optionalTrimmedString(matrixManifest.version) ?? undefined;
const runtime = asRecord(matrixManifest.runtime) as IMatrixRuntimeManifest | null;
const factorySource = readFactorySource(runtime?.factory, matrixManifestPath);
const serviceManifest = factorySource.manifest;
const packageNamespace = optionalTrimmedString(matrixManifest.namespace);
const runtimeEntry = optionalTrimmedString(runtime?.entry);
const components = Array.isArray(matrixManifest.components)
? matrixManifest.components.filter((entry): entry is Record<string, unknown> => isRecord(entry))
: [];
const packageRoot = readBindingDeclaration(asRecord(matrixManifest.root));
const packageComponents = components
.map((entry) => readBindingDeclaration(entry))
.filter((entry): entry is ILoadedMatrixBindingDeclaration => entry !== null);
const kind = optionalTrimmedString(serviceManifest.kind) ?? 'factory';
if (kind !== 'factory') {
throw new Error(`${factorySource.label} kind must be "factory", got "${kind}"`);
}
const entry = optionalTrimmedString(serviceManifest.entry) ?? runtimeEntry;
if (!entry) {
throw new Error(`${factorySource.label} requires entry or matrix.json runtime.entry`);
}
const exportName = optionalTrimmedString(serviceManifest.export);
if (!exportName) {
throw new Error(`${factorySource.label} export must be a non-empty string`);
}
const bootstrap = asRecord(serviceManifest.bootstrap);
assertUnsupportedStandaloneRecordRelocation({
sourceLabel: factorySource.label,
oldPath: 'overrides',
oldValue: serviceManifest.overrides,
canonicalPath: 'bootstrap.overrides',
});
const overrides = asRecord(bootstrap?.overrides) ?? {};
assertUnsupportedStandaloneStringRelocation({
sourceLabel: factorySource.label,
oldPath: 'root',
oldValue: serviceManifest.root,
canonicalPath: 'bootstrap.root',
});
const root = optionalTrimmedString(bootstrap?.root) ?? undefined;
assertUnsupportedStandaloneRecordRelocation({
sourceLabel: factorySource.label,
oldPath: 'transport',
oldValue: serviceManifest.transport,
canonicalPath: 'bootstrap.transport',
});
const transport = asRecord(bootstrap?.transport);
assertUnsupportedStandaloneRecordRelocation({
sourceLabel: factorySource.label,
oldPath: 'auth',
oldValue: serviceManifest.auth,
canonicalPath: 'bootstrap.auth',
});
const auth = asRecord(bootstrap?.auth);
assertUnsupportedStandaloneRecordRelocation({
sourceLabel: factorySource.label,
oldPath: 'http',
oldValue: serviceManifest.http,
canonicalPath: 'bootstrap.http',
});
const http = asRecord(bootstrap?.http);
const instance = asRecord(serviceManifest.instance);
const instancePackageName = optionalTrimmedString(instance?.packageName) ?? packageName;
const declaredPackageRevision = optionalTrimmedString(instance?.packageRevision);
const instancePackageRevision = declaredPackageRevision ?? packageVersion;
if (optionalTrimmedString(instance?.packageName) && instancePackageName !== packageName) {
throw new Error(`${factorySource.label} instance.packageName "${instancePackageName}" does not match matrix.json name "${packageName}"`);
}
if (declaredPackageRevision && packageVersion && declaredPackageRevision !== packageVersion) {
throw new Error(`${factorySource.label} instance.packageRevision "${declaredPackageRevision}" does not match matrix.json version "${packageVersion}"`);
}
const rootKind = optionalTrimmedString(instance?.rootKind) ?? 'package-root';
const componentType = optionalTrimmedString(instance?.componentType);
const instanceMount = optionalTrimmedString(instance?.mount);
assertUnsupportedStandaloneStringRelocation({
sourceLabel: factorySource.label,
oldPath: 'mount',
oldValue: serviceManifest.mount,
canonicalPath: 'instance.mount',
});
const mount = instanceMount
?? (rootKind === 'component'
? resolveComponentRootMount(factorySource.label, components, componentType)
: undefined);
const instanceConfigRef = optionalTrimmedString(instance?.configRef);
assertUnsupportedStandaloneStringRelocation({
sourceLabel: factorySource.label,
oldPath: 'config.file',
oldValue: asRecord(serviceManifest.config)?.file,
canonicalPath: 'instance.configRef',
});
const configRef = instanceConfigRef;
assertUnsupportedStandaloneStringRelocation({
sourceLabel: factorySource.label,
oldPath: 'secrets.file',
oldValue: asRecord(serviceManifest.secrets)?.file,
canonicalPath: 'instance.secretRefs or CLI --secrets',
});
const serviceClass = optionalTrimmedString(instance?.class) ?? 'service';
const instanceDependsOn = normalizeStringArray(instance?.dependsOn);
const instancePolicyRef = optionalTrimmedString(instance?.policyRef);
assertUnsupportedStandaloneArrayRelocation({
sourceLabel: factorySource.label,
oldPath: 'dependsOn',
oldValue: serviceManifest.dependsOn,
canonicalPath: 'instance.dependsOn',
});
assertUnsupportedStandaloneStringRelocation({
sourceLabel: factorySource.label,
oldPath: 'policyRef',
oldValue: serviceManifest.policyRef,
canonicalPath: 'instance.policyRef',
});
const serviceInstance = normalizeMountedInstanceSpec({
id: optionalTrimmedString(instance?.id) ?? buildServiceInstanceId(instancePackageName, mount),
packageName: instancePackageName,
packageRevision: instancePackageRevision,
class: serviceClass,
mount,
rootKind,
componentType,
configRef,
secretRefs: Array.isArray(instance?.secretRefs) ? instance.secretRefs : undefined,
dependsOn: instanceDependsOn.length > 0 ? instanceDependsOn : undefined,
policyRef: instancePolicyRef,
autoStart: instance?.autoStart === true,
}, {
registrationSource: 'matrix-service',
}) as IServiceInstanceBootstrap;
const entryPath = path.resolve(resolvedPackageDir, entry);
if (!fs.existsSync(entryPath)) {
throw new Error(`${factorySource.label} entry does not exist: ${entry}`);
}
return {
packageDir: resolvedPackageDir,
packageName,
...(displayName ? { displayName } : {}),
...(packageNamespace ? { packageNamespace } : {}),
manifestPath: factorySource.path,
entryPath,
exportName,
kind: 'factory',
overrides: structuredClone(overrides),
...(root ? { root } : {}),
...(mount ? { mount } : {}),
...(configRef ? { configRef } : {}),
...(transport ? { transport: structuredClone(transport) } : {}),
...(auth ? { auth: structuredClone(auth) } : {}),
...(http ? { http: structuredClone(http) } : {}),
...(packageRoot ? { packageRoot } : {}),
packageComponents,
serviceInstance,
};
}
function readFactorySource(factory: unknown, matrixManifestPath: string): {
readonly label: string;
readonly path: string;
readonly manifest: IMatrixServiceManifest;
} {
if (isRecord(factory)) {
return {
label: 'matrix.json runtime.factory',
path: matrixManifestPath,
manifest: factory as IMatrixServiceManifest,
};
}
throw new Error('matrix.json runtime.factory must be a JSON object');
}
function readJsonObject(filePath: string, label: string): JsonRecord {
const parsed = readJsonValue(filePath, label);
if (!isRecord(parsed)) {
throw new Error(`${label} must contain a JSON object`);
}
return parsed;
}
function readOptionalJsonObject(filePath: string, label: string): JsonRecord | null {
const parsed = readJsonValue(filePath, label);
if (!isRecord(parsed)) {
return null;
}
return parsed;
}
function readJsonValue(filePath: string, label: string): unknown {
if (!fs.existsSync(filePath)) {
throw new Error(`${label} not found at ${filePath}`);
}
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
return parsed;
} catch (err) {
throw new Error(`Failed to parse ${label}: ${err instanceof Error ? err.message : String(err)}`);
}
}
function optionalTrimmedString(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
return value;
}
function resolveComponentRootMount(
sourceLabel: string,
components: ReadonlyArray<Record<string, unknown>>,
componentType: string | undefined,
): string | undefined {
if (components.length === 0) {
throw new Error(`${sourceLabel} declares instance.rootKind="component", but matrix.json does not declare any components`);
}
if (componentType) {
const matching = components.find((entry) => optionalTrimmedString(entry.type) === componentType);
if (!matching) {
throw new Error(`${sourceLabel} instance.componentType "${componentType}" was not found in matrix.json components`);
}
const mount = optionalTrimmedString(matching.mount);
if (!mount) {
throw new Error(`${sourceLabel} instance.componentType "${componentType}" does not declare a component mount in matrix.json`);
}
return mount;
}
if (components.length === 1) {
const mount = optionalTrimmedString(components[0]?.mount);
if (!mount) {
throw new Error(`${sourceLabel} component-root bootstrap requires an explicit mount because the only matrix.json component does not declare one`);
}
return mount;
}
throw new Error(`${sourceLabel} component-root bootstrap is ambiguous; declare instance.componentType or mount`);
}
function readBindingDeclaration(
value: Record<string, unknown> | null | undefined,
): ILoadedMatrixBindingDeclaration | null {
if (!value) {
return null;
}
const mount = optionalTrimmedString(value.mount);
if (!mount) {
return null;
}
return {
...(optionalTrimmedString(value.type) ? { type: optionalTrimmedString(value.type) } : {}),
mount,
accepts: normalizeAccepts(value.accepts),
};
}
function normalizeAccepts(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
const accepts = value
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
return [...new Set(accepts)];
}
function buildServiceInstanceId(packageName: string, mount: string | undefined): string {
return mount ? `${packageName}:${mount}` : packageName;
}
function assertUnsupportedStandaloneStringRelocation(options: {
readonly sourceLabel: string;
readonly oldPath: string;
readonly oldValue: unknown;
readonly canonicalPath: string;
}): void {
if (optionalTrimmedString(options.oldValue) === undefined) return;
throw new Error(
`${options.sourceLabel} top-level "${options.oldPath}" is no longer supported; use "${options.canonicalPath}"`,
);
}
function assertUnsupportedStandaloneArrayRelocation(options: {
readonly sourceLabel: string;
readonly oldPath: string;
readonly oldValue: unknown;
readonly canonicalPath: string;
}): void {
if (normalizeStringArray(options.oldValue).length === 0) return;
throw new Error(
`${options.sourceLabel} top-level "${options.oldPath}" is no longer supported; use "${options.canonicalPath}"`,
);
}
function assertUnsupportedStandaloneRecordRelocation(options: {
readonly sourceLabel: string;
readonly oldPath: string;
readonly oldValue: unknown;
readonly canonicalPath: string;
}): void {
if (!isRecord(options.oldValue)) return;
throw new Error(
`${options.sourceLabel} top-level "${options.oldPath}" is no longer supported; use "${options.canonicalPath}"`,
);
}
function normalizeStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
const seen = new Set<string>();
const normalized: string[] = [];
for (const entry of value) {
const trimmed = optionalTrimmedString(entry);
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
normalized.push(trimmed);
}
return normalized;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View File

@ -0,0 +1,28 @@
export function compareSemver(left: string, right: string): number {
const leftParts = left.split('.').map((segment) => Number.parseInt(segment, 10));
const rightParts = right.split('.').map((segment) => Number.parseInt(segment, 10));
const length = Math.max(leftParts.length, rightParts.length);
for (let i = 0; i < length; i++) {
const a = Number.isFinite(leftParts[i]) ? leftParts[i]! : 0;
const b = Number.isFinite(rightParts[i]) ? rightParts[i]! : 0;
if (a > b) return 1;
if (a < b) return -1;
}
return 0;
}
export function maxSemver(versions: string[]): string | null {
if (versions.length === 0) {
return null;
}
let max = versions[0]!;
for (const version of versions.slice(1)) {
if (compareSemver(version, max) > 0) {
max = version;
}
}
return max;
}

View File

@ -0,0 +1,75 @@
import { strict as assert } from 'node:assert';
export interface IRunMxCliResult {
readonly logs: string[];
readonly errors: string[];
readonly exitCode: number;
}
async function loadMxCli(): Promise<{ createProgram: () => import('commander').Command }> {
const mxCliUrl = new URL('../../src/index.ts', import.meta.url).href;
return await import(`${mxCliUrl}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
}
export async function runMxCli(argv: string[], options: { cwd?: string } = {}): Promise<IRunMxCliResult> {
const logs: string[] = [];
const errors: string[] = [];
let exitCode = 0;
const originalLog = console.log;
const originalError = console.error;
const originalExit = process.exit;
const originalCwd = process.cwd();
const { createProgram } = await loadMxCli();
console.log = (...args: unknown[]) => {
logs.push(args.map((arg) => String(arg)).join(' '));
};
console.error = (...args: unknown[]) => {
errors.push(args.map((arg) => String(arg)).join(' '));
};
(process as unknown as { exit: (code?: number) => never }).exit = ((code?: number) => {
exitCode = code ?? 0;
throw new Error(`process.exit(${exitCode})`);
}) as never;
if (options.cwd) {
process.chdir(options.cwd);
}
try {
const program = createProgram();
await program.parseAsync(argv);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (!message.startsWith('process.exit(')) {
throw err;
}
} finally {
if (options.cwd) {
process.chdir(originalCwd);
}
console.log = originalLog;
console.error = originalError;
(process as unknown as { exit: typeof process.exit }).exit = originalExit;
}
return { logs, errors, exitCode };
}
export function parseLastJson(lines: readonly string[]): unknown {
for (let i = lines.length - 1; i >= 0; i--) {
const value = lines[i]?.trim() ?? '';
if (!value) continue;
try {
return JSON.parse(value);
} catch {
// continue
}
}
throw new Error(`No JSON payload found in output:\n${lines.join('\n')}`);
}
export function assertExitCode(result: IRunMxCliResult, expected: number): void {
assert.equal(result.exitCode, expected, `Expected exit code ${expected}, got ${result.exitCode}`);
}

View File

@ -0,0 +1,73 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
export interface ICreatePackageOptions {
readonly packageName: string;
readonly version?: string;
readonly mount?: string;
readonly componentType?: string;
readonly runtimeEntry?: string;
}
export function createMatrixPackage(rootDir: string, options: ICreatePackageOptions): {
readonly packageDir: string;
readonly packageName: string;
readonly version: string;
} {
const version = options.version ?? '1.0.0';
const componentType = options.componentType ?? 'SampleActor';
const mount = options.mount ?? 'sample.actor';
const runtimeEntry = options.runtimeEntry ?? 'src/index.ts';
const packageDir = path.join(rootDir, options.packageName.replace(/^@/, '').replace('/', '-'));
fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: options.packageName,
version,
runtime: { language: 'typescript', entry: runtimeEntry },
components: [
{
type: componentType,
export: componentType,
mount,
autoStart: true,
},
],
permissions: { fsPolicy: 'none' },
},
null,
2
),
'utf8'
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: options.packageName,
version,
type: 'module',
main: 'src/index.ts',
},
null,
2
),
'utf8'
);
fs.writeFileSync(
path.join(packageDir, 'src', 'index.ts'),
`export class ${componentType} {}\n`,
'utf8'
);
return {
packageDir,
packageName: options.packageName,
version,
};
}

View File

@ -0,0 +1,589 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
interface INpmPublishRegistryFixture {
readonly registryUrl: string;
readonly close: () => Promise<void>;
readonly publishRequests: () => number;
readonly tarballRequests: () => number;
readonly authorizationHeaders: () => readonly string[];
}
interface IPublishedPackage {
readonly name: string;
readonly version: string;
readonly fileName: string;
readonly tarball: Buffer;
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function readString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value : null;
}
function addressPort(server: http.Server): number {
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Fixture registry did not bind a TCP port');
}
return address.port;
}
async function readRequestBody(req: http.IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks).toString('utf8');
}
async function startNpmPublishRegistryFixture(): Promise<INpmPublishRegistryFixture> {
let publishRequests = 0;
let tarballRequests = 0;
let published: IPublishedPackage | null = null;
const authorizationHeaders: string[] = [];
const server = http.createServer((req, res) => {
void (async () => {
const auth = req.headers.authorization;
if (typeof auth === 'string') {
authorizationHeaders.push(auth);
}
const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
const url = new URL(req.url ?? '/', registryUrl);
if (req.method === 'PUT') {
publishRequests += 1;
const body = asRecord(JSON.parse(await readRequestBody(req)) as unknown);
const packageName = readString(body?.name);
const distTags = asRecord(body?.['dist-tags']);
const version = readString(distTags?.latest);
const attachments = asRecord(body?._attachments);
const attachmentEntry = attachments ? Object.entries(attachments)[0] : undefined;
const attachment = asRecord(attachmentEntry?.[1]);
const data = readString(attachment?.data);
if (!packageName || !version || !attachmentEntry || !data) {
res.writeHead(400, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'invalid publish payload' }));
return;
}
published = {
name: packageName,
version,
fileName: attachmentEntry[0],
tarball: Buffer.from(data, 'base64'),
};
res.writeHead(201, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === 'GET' && url.pathname.endsWith('.tgz')) {
tarballRequests += 1;
if (!published) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'no tarball published' }));
return;
}
res.writeHead(200, { 'content-type': 'application/octet-stream' });
res.end(published.tarball);
return;
}
const decodedPath = decodeURIComponent(url.pathname);
if (req.method === 'GET' && published && decodedPath === `/${published.name}`) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
name: published.name,
'dist-tags': { latest: published.version },
versions: {
[published.version]: {
name: published.name,
version: published.version,
dist: {
tarball: new URL(`tarballs/${published.fileName}`, registryUrl).toString(),
},
},
},
}));
return;
}
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not found', path: url.pathname }));
})().catch((error: unknown) => {
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
});
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
return {
registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
close: async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
publishRequests: () => publishRequests,
tarballRequests: () => tarballRequests,
authorizationHeaders: () => authorizationHeaders,
};
}
describe('FP-30 mx auth + publish integration', { concurrency: false }, () => {
const tempDirs: string[] = [];
const restoreEnvFns: Array<() => void> = [];
afterEach(() => {
while (restoreEnvFns.length > 0) {
const restore = restoreEnvFns.pop();
if (restore) {
restore();
}
}
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
function useMatrixHome(matrixHome: string): void {
const previousMatrixHome = process.env.MATRIX_HOME;
const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
const previousNodeAuthToken = process.env.NODE_AUTH_TOKEN;
const previousNpmToken = process.env.NPM_TOKEN;
process.env.MATRIX_HOME = matrixHome;
delete process.env.MATRIX_CLI_CONFIG;
delete process.env.MATRIX_PACKAGE_REGISTRY;
delete process.env.MATRIX_REGISTRY_URL;
delete process.env.NPM_CONFIG_USERCONFIG;
delete process.env.NODE_AUTH_TOKEN;
delete process.env.NPM_TOKEN;
restoreEnvFns.push(() => {
restoreEnv('MATRIX_HOME', previousMatrixHome);
restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig);
restoreEnv('NODE_AUTH_TOKEN', previousNodeAuthToken);
restoreEnv('NPM_TOKEN', previousNpmToken);
});
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}
it('requires login before publish and enables registry install after publish', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-auth-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const homeDir = path.join(root, 'home');
const previousHome = process.env.HOME;
fs.mkdirSync(homeDir, { recursive: true });
process.env.HOME = homeDir;
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-auth-publish',
version: '1.0.0',
componentType: 'AuthPublishActor',
mount: 'auth.publish',
});
const publishWithoutLogin = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishWithoutLogin, 1);
assert.equal(publishWithoutLogin.errors.some((line) => line.includes('MX_AUTH_REQUIRED')), true);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-auth-publish',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const installFromRegistry = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromRegistry, 0);
const payload = parseLastJson(installFromRegistry.logs) as { targetDir: string };
assert.equal(fs.existsSync(payload.targetDir), true);
} finally {
restoreEnv('HOME', previousHome);
}
});
it('publishes to an npm registry API and installs from the published tarball', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npm-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startNpmPublishRegistryFixture();
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-npm-publish',
version: '1.0.0',
componentType: 'NpmPublishActor',
mount: 'npm.publish',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-npm-publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryUrl?: string;
};
assert.equal(publishPayload.packageName, fixture.packageName);
assert.equal(publishPayload.version, fixture.version);
assert.equal(publishPayload.registryUrl, registry.registryUrl);
assert.equal(registry.publishRequests(), 1);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-npm-publish')),
true
);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 0);
const installPayload = parseLastJson(installResult.logs) as { targetDir: string };
assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true);
assert.equal(registry.tarballRequests(), 1);
} finally {
await registry.close();
}
});
it('keeps Matrix scoped packages on the requested npm registry API', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-scoped-npm-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
const homeDir = path.join(root, 'home');
fs.mkdirSync(workspaceDir, { recursive: true });
fs.mkdirSync(homeDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startNpmPublishRegistryFixture();
const previousHome = process.env.HOME;
const previousGlobalConfig = process.env.NPM_CONFIG_GLOBALCONFIG;
const wrongRegistry = 'http://127.0.0.1:9/wrong/npm/';
const poisonedGlobalConfig = path.join(root, 'poisoned-global-npmrc');
fs.writeFileSync(
path.join(homeDir, '.npmrc'),
[
`@matrix:registry=${wrongRegistry}`,
`@open-matrix:registry=${wrongRegistry}`,
`@omega:registry=${wrongRegistry}`,
'',
].join('\n'),
'utf8',
);
fs.writeFileSync(
poisonedGlobalConfig,
[
`@matrix:registry=${wrongRegistry}`,
`@open-matrix:registry=${wrongRegistry}`,
`@omega:registry=${wrongRegistry}`,
'',
].join('\n'),
'utf8',
);
process.env.HOME = homeDir;
process.env.NPM_CONFIG_GLOBALCONFIG = poisonedGlobalConfig;
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@matrix/fp30-scoped-npm-publish',
version: '1.0.0',
componentType: 'ScopedNpmPublishActor',
mount: 'scoped.npm.publish',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-scoped-npm-publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
assert.equal(registry.publishRequests(), 1);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-scoped-npm-publish')),
true,
);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 0);
const installPayload = parseLastJson(installResult.logs) as { targetDir: string };
assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true);
assert.equal(registry.tarballRequests(), 1);
} finally {
restoreEnv('HOME', previousHome);
restoreEnv('NPM_CONFIG_GLOBALCONFIG', previousGlobalConfig);
await registry.close();
}
});
it('publishes to the configured npm registry API without a per-command registry flag', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-configured-npm-publish-'));
tempDirs.push(root);
useMatrixHome(path.join(root, '.matrix'));
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startNpmPublishRegistryFixture();
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-configured-npm-publish',
version: '1.0.0',
componentType: 'ConfiguredNpmPublishActor',
mount: 'configured.npm.publish',
});
const setConfig = await runMxCli([
'node',
'mx',
'config',
'set',
'registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(setConfig, 0);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-configured-npm-publish',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryUrl?: string;
};
assert.equal(publishPayload.packageName, fixture.packageName);
assert.equal(publishPayload.version, fixture.version);
assert.equal(publishPayload.registryUrl, registry.registryUrl);
assert.equal(registry.publishRequests(), 1);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-configured-npm-publish')),
true,
);
} finally {
await registry.close();
}
});
it('publishes to an npm registry API using an npmrc token', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npmrc-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-npmrc-publish',
version: '1.0.0',
componentType: 'NpmrcPublishActor',
mount: 'npmrc.publish',
});
const registry = await startNpmPublishRegistryFixture();
const npmrcPath = path.join(root, '.npmrc');
const registryKey = registry.registryUrl.replace(/^https?:/, '');
const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
restoreEnvFns.push(() => restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig));
fs.writeFileSync(
npmrcPath,
`${registryKey}:_authToken=npmrc-token-publish\n`,
'utf8',
);
process.env.NPM_CONFIG_USERCONFIG = npmrcPath;
try {
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry',
registry.registryUrl,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 0);
assert.equal(registry.publishRequests(), 1);
assert.equal(registry.tarballRequests(), 1);
assert.equal(registry.authorizationHeaders().includes('Bearer npmrc-token-publish'), true);
} finally {
await registry.close();
}
});
});

View File

@ -0,0 +1,73 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
describe('FP-30 mx fork lifecycle', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('creates a fork with provenance metadata and renamed manifest surface', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-fork-'));
tempDirs.push(root);
const cwd = path.join(root, 'workspace');
const packagesDir = path.join(root, '.matrix', 'packages');
fs.mkdirSync(cwd, { recursive: true });
const source = createMatrixPackage(root, {
packageName: '@matrix/filesystem',
version: '1.0.0',
componentType: 'FolderProjection',
mount: 'fs',
});
const installResult = await runMxCli([
'node',
'mx',
'install',
source.packageDir,
'--packages-dir',
packagesDir,
'--json',
], { cwd });
assertExitCode(installResult, 0);
const forkResult = await runMxCli([
'node',
'mx',
'fork',
'@matrix/filesystem',
'--name',
'@alice/filesystem',
'--packages-dir',
packagesDir,
'--forked-by',
'alice',
'--json',
], { cwd });
assertExitCode(forkResult, 0);
const forkPayload = parseLastJson(forkResult.logs) as { targetDir: string };
assert.equal(fs.existsSync(forkPayload.targetDir), true);
assert.equal(fs.existsSync(path.join(forkPayload.targetDir, '.forked-from')), true);
const manifest = JSON.parse(fs.readFileSync(path.join(forkPayload.targetDir, 'matrix.json'), 'utf8')) as {
name: string;
components: Array<{ type?: string; export?: string; mount?: string }>;
};
assert.equal(manifest.name, '@alice/filesystem');
assert.equal(manifest.components[0]?.type, 'AliceFolderProjection');
assert.equal(manifest.components[0]?.export, 'AliceFolderProjection');
assert.equal(manifest.components[0]?.mount, 'alice.fs');
});
});

View File

@ -0,0 +1,146 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
function createLifecyclePackage(rootDir: string, packageName: string, scriptBody: string): string {
const packageDir = path.join(rootDir, packageName.replace(/^@/, '').replace('/', '-'));
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: packageName,
version: '1.0.0',
runtime: { language: 'javascript', entry: 'dist/index.js' },
components: [],
install: {
seed: {
script: 'dist/seed.js',
description: 'seed test hook',
},
},
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: packageName,
version: '1.0.0',
type: 'module',
main: 'dist/index.js',
},
null,
2,
),
'utf8',
);
fs.writeFileSync(path.join(packageDir, 'dist', 'index.js'), 'export const ready = true;\n', 'utf8');
fs.writeFileSync(path.join(packageDir, 'dist', 'seed.js'), scriptBody, 'utf8');
return packageDir;
}
describe('FP-30 mx install lifecycle', () => {
const tempDirs: string[] = [];
afterEach(() => {
delete process.env.MX_TEST_MARKER;
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('runs install lifecycle hooks and exposes install env', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-lifecycle-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const markerPath = path.join(root, 'seed-marker.json');
process.env.MX_TEST_MARKER = markerPath;
const packageDir = createLifecyclePackage(
root,
'@acme/install-lifecycle',
`import { writeFileSync } from 'node:fs';
const marker = process.env.MX_TEST_MARKER;
if (!marker) throw new Error('missing marker');
writeFileSync(marker, JSON.stringify({
phase: process.env.MATRIX_INSTALL_PHASE,
packagesDir: process.env.MATRIX_PACKAGES_DIR,
packageDir: process.env.MATRIX_PACKAGE_DIR,
packageName: process.env.MATRIX_PACKAGE_NAME
}), 'utf8');
`,
);
const result = await runMxCli([
'node',
'mx',
'install',
packageDir,
'--packages-dir',
packagesDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as {
targetDir: string;
lifecyclePhases: string[];
};
assert.deepEqual(payload.lifecyclePhases, ['seed']);
assert.equal(fs.existsSync(payload.targetDir), true);
assert.equal(fs.existsSync(markerPath), true);
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8')) as Record<string, string>;
assert.equal(marker.phase, 'seed');
assert.equal(path.resolve(marker.packagesDir), path.resolve(packagesDir));
assert.equal(path.resolve(marker.packageDir), path.resolve(payload.targetDir));
assert.equal(marker.packageName, '@acme/install-lifecycle');
});
it('rolls back package install when a lifecycle hook fails', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-lifecycle-fail-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const packageDir = createLifecyclePackage(
root,
'@acme/install-lifecycle-fail',
`throw new Error('seed failed');\n`,
);
const result = await runMxCli([
'node',
'mx',
'install',
packageDir,
'--packages-dir',
packagesDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(result, 1);
assert.equal(
result.errors.some((line) => line.includes('install.seed failed')),
true,
`Expected lifecycle failure in stderr, got:\n${result.errors.join('\n')}`,
);
const installedPath = path.join(packagesDir, 'node_modules', '@acme', 'install-lifecycle-fail');
assert.equal(fs.existsSync(installedPath), false);
});
});

View File

@ -0,0 +1,611 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
async function startNpmRegistryFixture(params: {
readonly packageName: string;
readonly version: string;
readonly tarballPath: string;
readonly expectedToken?: string;
}): Promise<{
readonly registryUrl: string;
readonly metadataRequests: () => number;
readonly tarballRequests: () => number;
readonly authorizationHeaders: () => readonly string[];
close(): Promise<void>;
}> {
const tarball = fs.readFileSync(params.tarballPath);
const shasum = createHash('sha1').update(tarball).digest('hex');
const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`;
let metadataRequestCount = 0;
let tarballRequestCount = 0;
const authorizationHeaders: string[] = [];
const server = http.createServer((request, response) => {
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
const decodedPath = decodeURIComponent(requestUrl.pathname);
const authorization = request.headers.authorization;
if (typeof authorization === 'string') {
authorizationHeaders.push(authorization);
} else if (Array.isArray(authorization)) {
authorizationHeaders.push(...authorization);
}
if (decodedPath === `/${params.packageName}`) {
metadataRequestCount += 1;
const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
const metadata = {
name: params.packageName,
'dist-tags': { latest: params.version },
versions: {
[params.version]: {
name: params.packageName,
version: params.version,
dist: {
tarball: new URL('tarballs/package.tgz', registryUrl).toString(),
shasum,
integrity,
},
},
},
};
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(metadata));
return;
}
if (decodedPath === '/tarballs/package.tgz') {
tarballRequestCount += 1;
response.writeHead(200, {
'content-type': 'application/octet-stream',
'content-length': String(tarball.byteLength),
});
response.end(tarball);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: `Not found: ${decodedPath}` }));
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
return {
registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
metadataRequests: () => metadataRequestCount,
tarballRequests: () => tarballRequestCount,
authorizationHeaders: () => [...authorizationHeaders],
async close(): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
function addressPort(server: http.Server): number {
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Registry fixture is not listening on a TCP port');
}
return address.port;
}
function npmPackPackage(packageDir: string, outDir: string): string {
fs.mkdirSync(outDir, { recursive: true });
const result = spawnSync('npm', ['pack', packageDir, '--pack-destination', outDir], {
encoding: 'utf8',
});
assert.equal(
result.status,
0,
`npm pack failed\nstdout:\n${result.stdout ?? ''}\nstderr:\n${result.stderr ?? ''}`,
);
const fileName = result.stdout.trim().split(/\r?\n/).filter(Boolean).pop();
assert.equal(Boolean(fileName), true);
return path.join(outDir, fileName!);
}
describe('FP-30 mx install source resolution', () => {
const tempDirs: string[] = [];
const closeFns: Array<() => Promise<void>> = [];
const restoreEnvFns: Array<() => void> = [];
afterEach(async () => {
while (restoreEnvFns.length > 0) {
const restore = restoreEnvFns.pop();
if (restore) {
restore();
}
}
while (closeFns.length > 0) {
const close = closeFns.pop();
if (close) {
await close();
}
}
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
function useMatrixHome(matrixHome: string): void {
const previousMatrixHome = process.env.MATRIX_HOME;
const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
process.env.MATRIX_HOME = matrixHome;
delete process.env.MATRIX_CLI_CONFIG;
delete process.env.MATRIX_PACKAGE_REGISTRY;
delete process.env.MATRIX_REGISTRY_URL;
restoreEnvFns.push(() => {
restoreEnv('MATRIX_HOME', previousMatrixHome);
restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
});
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}
it('installs package from local directory, tarball, and registry name', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-sources-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/install-source',
version: '1.0.0',
componentType: 'InstallSourceActor',
mount: 'install.source',
});
const installFromDirectory = await runMxCli([
'node',
'mx',
'install',
fixture.packageDir,
'--packages-dir',
packagesDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromDirectory, 0);
const installedFromDir = parseLastJson(installFromDirectory.logs) as { targetDir: string };
assert.equal(fs.existsSync(installedFromDir.targetDir), true);
const uninstallAfterDirectory = await runMxCli([
'node',
'mx',
'uninstall',
fixture.packageName,
'--packages-dir',
packagesDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(uninstallAfterDirectory, 0);
const packResult = await runMxCli([
'node',
'mx',
'pack',
'--out-dir',
path.join(root, 'packed'),
'--json',
], { cwd: fixture.packageDir });
assertExitCode(packResult, 0);
const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
assert.equal(fs.existsSync(packPayload.tarballPath), true);
const installFromTarball = await runMxCli([
'node',
'mx',
'install',
packPayload.tarballPath,
'--packages-dir',
packagesDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromTarball, 0);
const installedFromTarball = parseLastJson(installFromTarball.logs) as { targetDir: string };
assert.equal(fs.existsSync(installedFromTarball.targetDir), true);
const uninstallAfterTarball = await runMxCli([
'node',
'mx',
'uninstall',
fixture.packageName,
'--packages-dir',
packagesDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(uninstallAfterTarball, 0);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-install',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const installFromRegistry = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromRegistry, 0);
const installedFromRegistry = parseLastJson(installFromRegistry.logs) as { source: string; targetDir: string };
assert.equal(installedFromRegistry.source, `${fixture.packageName}@${fixture.version}`);
assert.equal(fs.existsSync(installedFromRegistry.targetDir), true);
});
it('installs package from an npm-compatible registry URL with stored registry credentials', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-npm-registry-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/remote-install',
version: '1.2.3',
componentType: 'RemoteInstallActor',
mount: 'remote.install',
});
fs.mkdirSync(path.join(fixture.packageDir, 'vendor', 'runtime-helper'), { recursive: true });
fs.writeFileSync(
path.join(fixture.packageDir, 'vendor', 'runtime-helper', 'package.json'),
JSON.stringify({
name: '@acme/runtime-helper',
version: '1.0.0',
type: 'module',
main: 'index.js',
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(fixture.packageDir, 'vendor', 'runtime-helper', 'index.js'),
'export const helper = true;\n',
'utf8',
);
const packageJsonPath = path.join(fixture.packageDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record<string, unknown>;
packageJson.dependencies = {
'@acme/runtime-helper': 'file:./vendor/runtime-helper',
};
packageJson.devDependencies = {
'@open-matrix/core': '*',
};
packageJson.peerDependencies = {
'@open-matrix/core': '*',
};
packageJson.peerDependenciesMeta = {
'@open-matrix/core': { optional: true },
};
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
const packResult = await runMxCli([
'node',
'mx',
'pack',
'--out-dir',
path.join(root, 'packed'),
'--json',
], { cwd: fixture.packageDir });
assertExitCode(packResult, 0);
const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
const registry = await startNpmRegistryFixture({
packageName: fixture.packageName,
version: fixture.version,
tarballPath: packPayload.tarballPath,
expectedToken: 'token-remote-install',
});
closeFns.push(registry.close);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-remote-install',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const installFromRemoteRegistry = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromRemoteRegistry, 0);
const installed = parseLastJson(installFromRemoteRegistry.logs) as {
source: string;
targetDir: string;
version: string;
};
assert.equal(installed.source, `${fixture.packageName}@${fixture.version}`);
assert.equal(installed.version, fixture.version);
assert.equal(fs.existsSync(installed.targetDir), true);
assert.equal(
fs.existsSync(path.join(installed.targetDir, 'node_modules', '@acme', 'runtime-helper', 'package.json')),
true,
);
assert.equal(
fs.existsSync(path.join(installed.targetDir, 'node_modules', '@open-matrix', 'core', 'package.json')),
false,
);
assert.equal(registry.metadataRequests() > 0, true);
assert.equal(registry.tarballRequests() > 0, true);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-remote-install')),
true,
);
});
it('installs package names from the configured npm registry without a per-command registry flag', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-config-registry-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
useMatrixHome(path.join(root, '.matrix'));
const packagesDir = path.join(root, '.matrix', 'packages');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/configured-remote-install',
version: '1.2.3',
componentType: 'ConfiguredRemoteInstallActor',
mount: 'configured.remote.install',
});
const packResult = await runMxCli([
'node',
'mx',
'pack',
'--out-dir',
path.join(root, 'packed'),
'--json',
], { cwd: fixture.packageDir });
assertExitCode(packResult, 0);
const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
const registry = await startNpmRegistryFixture({
packageName: fixture.packageName,
version: fixture.version,
tarballPath: packPayload.tarballPath,
expectedToken: 'token-configured-install',
});
closeFns.push(registry.close);
const setConfig = await runMxCli([
'node',
'mx',
'config',
'set',
'registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(setConfig, 0);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-configured-install',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const installFromConfiguredRegistry = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromConfiguredRegistry, 0);
const installed = parseLastJson(installFromConfiguredRegistry.logs) as {
source: string;
targetDir: string;
version: string;
};
assert.equal(installed.source, `${fixture.packageName}@${fixture.version}`);
assert.equal(installed.version, fixture.version);
assert.equal(fs.existsSync(installed.targetDir), true);
assert.equal(registry.metadataRequests() > 0, true);
assert.equal(registry.tarballRequests() > 0, true);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-configured-install')),
true,
);
});
it('rejects npm registry packages whose matrix.json and package.json versions disagree', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-version-mismatch-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/version-mismatch',
version: '1.0.0',
componentType: 'VersionMismatchActor',
mount: 'version.mismatch',
});
fs.writeFileSync(
path.join(fixture.packageDir, 'package.json'),
JSON.stringify({
name: fixture.packageName,
version: '9.9.9',
type: 'module',
main: 'src/index.ts',
}, null, 2),
'utf8',
);
const tarballPath = npmPackPackage(fixture.packageDir, path.join(root, 'packed'));
const registry = await startNpmRegistryFixture({
packageName: fixture.packageName,
version: '9.9.9',
tarballPath,
});
closeFns.push(registry.close);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@9.9.9`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 1);
assert.equal(
installResult.errors.some((line) => line.includes('MX_PACKAGE_VERSION_MISMATCH')),
true,
);
});
it('normalizes whitespace-padded registry specifiers before install', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-specifier-trim-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/install-source',
version: '1.0.0',
componentType: 'InstallSourceActor',
mount: 'install.source',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-install',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const installFromRegistry = await runMxCli([
'node',
'mx',
'install',
` ${fixture.packageName}@${fixture.version} `,
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromRegistry, 0);
const installedFromRegistry = parseLastJson(installFromRegistry.logs) as { source: string; targetDir: string };
assert.equal(installedFromRegistry.source, `${fixture.packageName}@${fixture.version}`);
assert.equal(fs.existsSync(installedFromRegistry.targetDir), true);
});
});

View File

@ -0,0 +1,167 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
describe('FP-30 mx lifecycle commands', () => {
const tempDirs: string[] = [];
function createLocalPackage(rootDir: string): { packageDir: string; packageName: string } {
const packageName = '@acme/weather-actor';
const packageDir = path.join(rootDir, 'weather-actor');
fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: packageName,
version: '1.0.0',
runtime: { language: 'typescript', entry: 'src/index.ts' },
components: [
{
type: 'WeatherActor',
export: 'WeatherActor',
mount: 'weather.actor',
autoStart: true,
},
],
permissions: { fsPolicy: 'none' },
},
null,
2
),
'utf8'
);
fs.writeFileSync(path.join(packageDir, 'package.json'), JSON.stringify({ name: packageName, version: '1.0.0' }, null, 2));
fs.writeFileSync(path.join(packageDir, 'src', 'index.ts'), 'export class WeatherActor {}\n', 'utf8');
return { packageDir, packageName };
}
async function loadMxCli(): Promise<{ createProgram: () => import('commander').Command }> {
const mxCliUrl = new URL('../../src/index.ts', import.meta.url).href;
return await import(`${mxCliUrl}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
}
async function runMxCli(argv: string[]): Promise<string[]> {
const logs: string[] = [];
const originalLog = console.log;
const originalExit = process.exit;
const { createProgram } = await loadMxCli();
console.log = (...args: unknown[]) => {
logs.push(args.map((arg) => String(arg)).join(' '));
};
(process as unknown as { exit: (code?: number) => never }).exit = ((code?: number) => {
throw new Error(`process.exit(${code ?? 0})`);
}) as never;
try {
const program = createProgram();
await program.parseAsync(argv);
return logs;
} finally {
console.log = originalLog;
(process as unknown as { exit: typeof process.exit }).exit = originalExit;
}
}
function parseLastJson(logs: string[]): unknown {
for (let i = logs.length - 1; i >= 0; i--) {
const value = logs[i].trim();
if (!value) continue;
try {
return JSON.parse(value);
} catch {
// Continue scanning log lines until JSON is found.
}
}
throw new Error(`No JSON payload found in logs:\n${logs.join('\n')}`);
}
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('installs, lists, inspects, validates, and uninstalls local packages', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-lifecycle-'));
tempDirs.push(root);
const cwd = path.join(root, 'workspace');
const packagesDir = path.join(cwd, '.matrix', 'packages');
fs.mkdirSync(cwd, { recursive: true });
const { packageDir, packageName } = createLocalPackage(root);
await runMxCli([
'node',
'mx',
'install',
packageDir,
'--packages-dir',
packagesDir,
'--json',
]);
const installedPath = path.join(packagesDir, 'node_modules', '@acme', 'weather-actor');
assert.equal(fs.existsSync(installedPath), true);
const listedAfterInstall = parseLastJson(await runMxCli([
'node',
'mx',
'list',
'--packages-dir',
packagesDir,
'--json',
])) as string[];
assert.equal(Array.isArray(listedAfterInstall), true);
assert.equal(listedAfterInstall.includes(packageName), true);
const infoPayload = parseLastJson(await runMxCli([
'node',
'mx',
'info',
packageName,
'--packages-dir',
packagesDir,
'--json',
])) as { packageName: string; installPath: string };
assert.equal(infoPayload.packageName, packageName);
assert.equal(path.resolve(infoPayload.installPath), path.resolve(installedPath));
const validatePayload = parseLastJson(await runMxCli([
'node',
'mx',
'validate',
path.join(packageDir, 'matrix.json'),
'--json',
])) as { packageName: string; valid: boolean };
assert.equal(validatePayload.packageName, packageName);
assert.equal(validatePayload.valid, true);
await runMxCli([
'node',
'mx',
'uninstall',
packageName,
'--packages-dir',
packagesDir,
'--json',
]);
assert.equal(fs.existsSync(installedPath), false);
const listedAfterUninstall = parseLastJson(await runMxCli([
'node',
'mx',
'list',
'--packages-dir',
packagesDir,
'--json',
])) as string[];
assert.equal(Array.isArray(listedAfterUninstall), true);
assert.equal(listedAfterUninstall.includes(packageName), false);
});
});

View File

@ -0,0 +1,142 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
describe('FP-30 mx outdated/update-fork', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('reports outdated forks and updates fork upstream version metadata', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-outdated-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const upstreamV1 = createMatrixPackage(path.join(root, 'upstream-v1'), {
packageName: '@matrix/filesystem',
version: '1.0.0',
componentType: 'FolderProjection',
mount: 'fs',
});
const upstreamV12 = createMatrixPackage(path.join(root, 'upstream-v12'), {
packageName: '@matrix/filesystem',
version: '1.2.0',
componentType: 'FolderProjection',
mount: 'fs',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-xyz',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
for (const pkg of [upstreamV1, upstreamV12]) {
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: pkg.packageDir });
assertExitCode(publishResult, 0);
}
const installOrigin = await runMxCli([
'node',
'mx',
'install',
'@matrix/filesystem@1.0.0',
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installOrigin, 0);
const forkResult = await runMxCli([
'node',
'mx',
'fork',
'@matrix/filesystem',
'--name',
'@alice/filesystem',
'--packages-dir',
packagesDir,
'--forked-by',
'alice',
'--json',
], { cwd: workspaceDir });
assertExitCode(forkResult, 0);
const outdatedResult = await runMxCli([
'node',
'mx',
'outdated',
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(outdatedResult, 0);
const outdatedPayload = parseLastJson(outdatedResult.logs) as Array<{
packageName: string;
status: string;
latestOriginVersion: string | null;
}>;
const forkEntry = outdatedPayload.find((entry) => entry.packageName === '@alice/filesystem');
assert.ok(forkEntry);
assert.equal(forkEntry?.status, 'outdated');
assert.equal(forkEntry?.latestOriginVersion, '1.2.0');
const updateResult = await runMxCli([
'node',
'mx',
'update-fork',
'@alice/filesystem',
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(updateResult, 0);
const updatePayload = parseLastJson(updateResult.logs) as {
updated: boolean;
previousOriginVersion: string;
nextOriginVersion: string;
};
assert.equal(updatePayload.updated, true);
assert.equal(updatePayload.previousOriginVersion, '1.0.0');
assert.equal(updatePayload.nextOriginVersion, '1.2.0');
});
});

View File

@ -0,0 +1,53 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
describe('FP-30 mx pack command', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('creates distributable tarball with package identity metadata', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-pack-'));
tempDirs.push(root);
const fixture = createMatrixPackage(root, {
packageName: '@acme/fp30-pack',
version: '2.3.4',
componentType: 'PackActor',
mount: 'fp30.pack',
});
const outDir = path.join(root, 'dist-pack');
const result = await runMxCli([
'node',
'mx',
'pack',
'--out-dir',
outDir,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as {
packageName: string;
version: string;
tarballPath: string;
};
assert.equal(payload.packageName, fixture.packageName);
assert.equal(payload.version, fixture.version);
assert.equal(fs.existsSync(payload.tarballPath), true);
assert.equal(payload.tarballPath.endsWith('.tar.gz'), true);
});
});

View File

@ -0,0 +1,342 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
describe('FP-30 mx publish preflight', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('requires login and enforces immutable version publish', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-'));
tempDirs.push(root);
const fixture = createMatrixPackage(root, {
packageName: '@acme/fp30-publish',
version: '1.0.0',
componentType: 'PublishActor',
mount: 'fp30.publish',
});
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const unauthenticatedPublish = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(unauthenticatedPublish, 1);
assert.equal(
unauthenticatedPublish.errors.some((line) => line.includes('MX_AUTH_REQUIRED')),
true
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-abc',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, fixture.packageName);
assert.equal(publishPayload.version, fixture.version);
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
const republishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(republishResult, 1);
assert.equal(republishResult.errors.some((line) => line.includes('MX_VERSION_EXISTS')), true);
});
it('allows webapp-only packages without actor components', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-webapp-'));
tempDirs.push(root);
const packageDir = path.join(root, 'webapp-only');
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(packageDir, 'dist', 'index.html'), '<!doctype html><title>Webapp only</title>\n', 'utf8');
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: '@acme/fp30-webapp-only',
version: '1.0.0',
runtime: { language: 'typescript', entry: 'dist/index.html' },
webapp: { distDir: 'dist', entry: 'index.html', appName: 'webapp-only' },
components: [],
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: '@acme/fp30-webapp-only',
version: '1.0.0',
type: 'module',
files: ['dist', 'matrix.json'],
},
null,
2,
),
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-webapp-only',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, '@acme/fp30-webapp-only');
assert.equal(publishPayload.version, '1.0.0');
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
});
it('allows runtime-only packages with a root actor and no components', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-runtime-only-'));
tempDirs.push(root);
const packageDir = path.join(root, 'runtime-only');
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(packageDir, 'dist', 'index.js'), 'export class RuntimeOnlyActor {}\n', 'utf8');
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: '@acme/fp30-runtime-only',
version: '1.0.0',
root: {
type: 'RuntimeOnlyActor',
export: 'RuntimeOnlyActor',
description: 'Runtime-only root actor',
},
runtime: { language: 'typescript', entry: 'dist/index.js' },
components: [],
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: '@acme/fp30-runtime-only',
version: '1.0.0',
type: 'module',
files: ['dist', 'matrix.json'],
},
null,
2,
),
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-runtime-only',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, '@acme/fp30-runtime-only');
assert.equal(publishPayload.version, '1.0.0');
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
});
it('allows service factory packages declared by matrix.json runtime.factory', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-service-'));
tempDirs.push(root);
const packageDir = path.join(root, 'service-factory');
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'dist', 'index.js'),
'export async function createServiceFactory() { return { runtime: { shutdown() {} } }; }\n',
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: '@acme/fp30-service-factory',
version: '1.0.0',
runtime: {
language: 'typescript',
entry: 'dist/index.js',
factory: {
kind: 'factory',
export: 'createServiceFactory',
instance: {
id: 'RUNTIME-SERVICE-FACTORY',
class: 'service',
rootKind: 'package-root',
autoStart: true,
},
},
},
components: [],
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: '@acme/fp30-service-factory',
version: '1.0.0',
type: 'module',
files: ['dist', 'matrix.json'],
},
null,
2,
),
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-service-factory',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, '@acme/fp30-service-factory');
assert.equal(publishPayload.version, '1.0.0');
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
});
});

View File

@ -0,0 +1,192 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
function addressPort(server: http.Server): number {
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Fixture registry did not bind a TCP port');
}
return address.port;
}
function asRecord(value: unknown): Record<string, unknown> {
assert.ok(value !== null && typeof value === 'object' && !Array.isArray(value));
return value as Record<string, unknown>;
}
async function startRegistryFixture(): Promise<{ readonly registryUrl: string; readonly close: () => Promise<void> }> {
const server = http.createServer((req, res) => {
const registryUrl = `http://127.0.0.1:${addressPort(server)}/api/packages/open-matrix/npm/`;
const url = new URL(req.url ?? '/', registryUrl);
if (req.method === 'GET' && url.pathname.endsWith('/-/whoami')) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ username: 'alice' }));
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
return {
registryUrl: `http://127.0.0.1:${addressPort(server)}/api/packages/open-matrix/npm/`,
close: async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
describe('FP-30 mx registry commands', { concurrency: false }, () => {
const tempDirs: string[] = [];
const restoreEnvFns: Array<() => void> = [];
const fixtureClosers: Array<() => Promise<void>> = [];
afterEach(async () => {
while (fixtureClosers.length > 0) {
const close = fixtureClosers.pop();
if (close) {
await close();
}
}
while (restoreEnvFns.length > 0) {
const restore = restoreEnvFns.pop();
if (restore) {
restore();
}
}
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
function useCleanEnv(root: string): void {
const previousHome = process.env.HOME;
const previousMatrixHome = process.env.MATRIX_HOME;
const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
const previousNodeAuthToken = process.env.NODE_AUTH_TOKEN;
const previousNpmToken = process.env.NPM_TOKEN;
const home = path.join(root, 'home');
fs.mkdirSync(home, { recursive: true });
process.env.HOME = home;
process.env.MATRIX_HOME = path.join(root, '.matrix');
delete process.env.MATRIX_CLI_CONFIG;
delete process.env.MATRIX_PACKAGE_REGISTRY;
delete process.env.MATRIX_REGISTRY_URL;
delete process.env.NPM_CONFIG_USERCONFIG;
delete process.env.NODE_AUTH_TOKEN;
delete process.env.NPM_TOKEN;
restoreEnvFns.push(() => {
restoreEnv('HOME', previousHome);
restoreEnv('MATRIX_HOME', previousMatrixHome);
restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig);
restoreEnv('NODE_AUTH_TOKEN', previousNodeAuthToken);
restoreEnv('NPM_TOKEN', previousNpmToken);
});
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}
it('uses a local Gitea registry profile and diagnoses scope config without reading poisoned user npmrc', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-registry-command-'));
tempDirs.push(root);
useCleanEnv(root);
fs.writeFileSync(path.join(process.env.HOME!, '.npmrc'), [
'@matrix:registry=https://wrong.example.invalid/',
'@open-matrix:registry=https://wrong.example.invalid/',
'@omega:registry=https://wrong.example.invalid/',
'',
].join('\n'), 'utf8');
const fixture = await startRegistryFixture();
fixtureClosers.push(fixture.close);
const credentialsFile = path.join(root, 'credentials.json');
const useResult = await runMxCli([
'node',
'mx',
'registry',
'use',
'local-gitea',
'--registry',
fixture.registryUrl,
'--json',
], { cwd: root });
assertExitCode(useResult, 0);
assert.equal((parseLastJson(useResult.logs) as Record<string, unknown>).registry, fixture.registryUrl);
const loginResult = await runMxCli([
'node',
'mx',
'registry',
'login',
'--registry',
fixture.registryUrl,
'--username',
'alice',
'--token',
'token-registry-command',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: root });
assertExitCode(loginResult, 0);
const doctorResult = await runMxCli([
'node',
'mx',
'registry',
'doctor',
'--registry',
fixture.registryUrl,
'--credentials-file',
credentialsFile,
'--timeout-ms',
'5000',
'--json',
], { cwd: root });
assertExitCode(doctorResult, 0);
const doctor = asRecord(parseLastJson(doctorResult.logs));
const registry = asRecord(doctor.registry);
const credentials = asRecord(doctor.credentials);
const npm = asRecord(doctor.npm);
assert.equal(doctor.ok, true);
assert.equal(registry.url, fixture.registryUrl);
assert.equal(credentials.found, true);
assert.equal(credentials.authOk, true);
assert.equal(npm.allScopesMatch, true);
assert.equal(npm.poisonedUserConfigIgnored, true);
assert.deepEqual(npm.scopeResolution, {
'@open-matrix': fixture.registryUrl,
'@matrix': fixture.registryUrl,
'@omega': fixture.registryUrl,
});
});
});

View File

@ -0,0 +1,54 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { actorElementCommand } from '../../src/commands/actor-element.js';
import { installOpenMatrixCoreStub } from '../support/installCoreStub.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../../../');
describe('FP-30 mx actor-element scaffold', () => {
it('creates paired actor and element scaffold package', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-element-'));
const outputDir = path.join(tmpRoot, 'weather-panel');
try {
const result = await actorElementCommand('weather-panel', {
cwd: repoRoot,
outputDir,
});
assert.equal(fs.existsSync(path.join(outputDir, 'matrix.json')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.actorClassName}.ts`)), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.elementClassName}.ts`)), true);
assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'query-value.mxq')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'render-element.mxq')), true);
const manifest = JSON.parse(fs.readFileSync(path.join(outputDir, 'matrix.json'), 'utf8')) as {
components: Array<{ mount?: string; type?: string }>;
};
assert.equal(manifest.components.length, 2);
assert.equal(manifest.components.some((entry) => entry.mount === 'weather-panel.actor'), true);
assert.equal(manifest.components.some((entry) => entry.mount === 'weather-panel.element'), true);
installOpenMatrixCoreStub(outputDir);
const compile = spawnSync(
process.execPath,
[
path.resolve(repoRoot, 'node_modules/tsx/dist/cli.mjs'),
path.join(outputDir, 'src', 'index.ts'),
],
{ cwd: outputDir, encoding: 'utf8' }
);
assert.equal(compile.status, 0, compile.stderr || '(no stderr)');
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
});

View File

@ -0,0 +1,54 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { cqrsCommand } from '../../src/commands/cqrs.js';
import { installOpenMatrixCoreStub } from '../support/installCoreStub.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../../../');
describe('FP-30 mx cqrs scaffold', () => {
it('creates command/query archetype package', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-cqrs-'));
const outputDir = path.join(tmpRoot, 'orders-cqrs');
try {
const result = await cqrsCommand('orders-cqrs', {
cwd: repoRoot,
outputDir,
});
assert.equal(fs.existsSync(path.join(outputDir, 'matrix.json')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.commandClassName}.ts`)), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.queryClassName}.ts`)), true);
assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'execute.mxq')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'query.mxq')), true);
const manifest = JSON.parse(fs.readFileSync(path.join(outputDir, 'matrix.json'), 'utf8')) as {
components: Array<{ mount?: string; type?: string }>;
};
assert.equal(manifest.components.length, 2);
assert.equal(manifest.components.some((entry) => entry.mount === 'orders-cqrs.command'), true);
assert.equal(manifest.components.some((entry) => entry.mount === 'orders-cqrs.query'), true);
installOpenMatrixCoreStub(outputDir);
const compile = spawnSync(
process.execPath,
[
path.resolve(repoRoot, 'node_modules/tsx/dist/cli.mjs'),
path.join(outputDir, 'src', 'index.ts'),
],
{ cwd: outputDir, encoding: 'utf8' }
);
assert.equal(compile.status, 0, compile.stderr || '(no stderr)');
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
});

View File

@ -0,0 +1,54 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { elementCommand } from '../../src/commands/element.js';
import { installOpenMatrixCoreStub } from '../support/installCoreStub.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../../../');
describe('FP-30 mx element scaffold', () => {
it('creates a compilable element package archetype', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-element-'));
const outputDir = path.join(tmpRoot, 'status-element');
try {
const result = await elementCommand('status-element', {
cwd: repoRoot,
outputDir,
});
assert.equal(fs.existsSync(path.join(outputDir, 'matrix.json')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.className}.ts`)), true);
assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'render.mxq')), true);
assert.equal(fs.existsSync(path.join(outputDir, 'skills', 'status-element.skill.md')), true);
const manifest = JSON.parse(fs.readFileSync(path.join(outputDir, 'matrix.json'), 'utf8')) as {
name: string;
components: Array<{ mount?: string; type?: string }>;
};
assert.equal(manifest.name, 'status-element');
assert.equal(manifest.components[0]?.mount, 'status-element');
assert.equal(manifest.components[0]?.type, result.className);
installOpenMatrixCoreStub(outputDir);
const compile = spawnSync(
process.execPath,
[
path.resolve(repoRoot, 'node_modules/tsx/dist/cli.mjs'),
path.join(outputDir, 'src', `${result.className}.ts`),
],
{ cwd: outputDir, encoding: 'utf8' }
);
assert.equal(compile.status, 0, compile.stderr || '(no stderr)');
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
});

View File

@ -0,0 +1,341 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
interface ISearchFixture {
readonly registryUrl: string;
readonly searchRequests: () => number;
readonly authorizationHeaders: () => readonly string[];
readonly searchQueries: () => readonly string[];
close(): Promise<void>;
}
function addressPort(server: http.Server): number {
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Search registry fixture is not listening on a TCP port');
}
return address.port;
}
async function startSearchRegistryFixture(): Promise<ISearchFixture> {
let searchRequestCount = 0;
const authorizationHeaders: string[] = [];
const searchQueries: string[] = [];
const server = http.createServer((request, response) => {
const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
const requestUrl = new URL(request.url ?? '/', registryUrl);
const auth = request.headers.authorization;
if (typeof auth === 'string') {
authorizationHeaders.push(auth);
}
if (requestUrl.pathname === '/-/v1/search') {
searchRequestCount += 1;
searchQueries.push(requestUrl.searchParams.get('text') ?? '');
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({
objects: [
{
package: {
name: '@open-matrix/chat',
version: '0.2.3',
description: 'Chat package',
keywords: ['matrix', 'chat'],
},
},
{
package: {
scope: '@open-matrix',
name: 'flowpad',
version: '0.3.0',
description: 'FlowPad package',
keywords: ['matrix', 'flowpad'],
},
},
{
package: {
name: '@open-matrix/director',
version: '0.1.0',
description: 'Director package',
keywords: ['matrix', 'director'],
},
},
],
}));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: `Not found: ${requestUrl.pathname}` }));
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
return {
registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
searchRequests: () => searchRequestCount,
authorizationHeaders: () => [...authorizationHeaders],
searchQueries: () => [...searchQueries],
async close(): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
describe('FP-30 mx search command', () => {
const tempDirs: string[] = [];
const closeFns: Array<() => Promise<void>> = [];
const restoreEnvFns: Array<() => void> = [];
afterEach(async () => {
while (restoreEnvFns.length > 0) {
const restore = restoreEnvFns.pop();
if (restore) {
restore();
}
}
while (closeFns.length > 0) {
const close = closeFns.pop();
if (close) {
await close();
}
}
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
function useMatrixHome(matrixHome: string): void {
const previousMatrixHome = process.env.MATRIX_HOME;
const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
process.env.MATRIX_HOME = matrixHome;
delete process.env.MATRIX_CLI_CONFIG;
delete process.env.MATRIX_PACKAGE_REGISTRY;
delete process.env.MATRIX_REGISTRY_URL;
restoreEnvFns.push(() => {
restoreEnv('MATRIX_HOME', previousMatrixHome);
restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
});
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}
it('searches the configured npm registry with stored registry credentials', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-search-'));
tempDirs.push(root);
useMatrixHome(path.join(root, '.matrix'));
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startSearchRegistryFixture();
closeFns.push(registry.close);
const setConfig = await runMxCli([
'node',
'mx',
'config',
'set',
'registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(setConfig, 0);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-search',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const searchResult = await runMxCli([
'node',
'mx',
'search',
'chat',
'--credentials-file',
credentialsFile,
'--limit',
'5',
'--json',
], { cwd: workspaceDir });
assertExitCode(searchResult, 0);
const payload = parseLastJson(searchResult.logs) as {
query: string;
registry: string;
registrySource: string;
results: Array<{ packageName: string; version: string; description?: string; keywords: string[] }>;
};
assert.equal(payload.query, 'chat');
assert.equal(payload.registry, registry.registryUrl);
assert.equal(payload.registrySource, 'config');
assert.equal(payload.results.length, 3);
assert.equal(payload.results[0]?.packageName, '@open-matrix/chat');
assert.equal(payload.results[0]?.version, '0.2.3');
assert.deepEqual(payload.results[0]?.keywords, ['matrix', 'chat']);
assert.equal(payload.results[1]?.packageName, '@open-matrix/flowpad');
assert.equal(payload.results[1]?.version, '0.3.0');
assert.equal(registry.searchRequests(), 1);
assert.deepEqual(registry.searchQueries(), ['chat']);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-search')),
true,
);
});
it('searches the local package discovery index created by publish --registry-dir', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-discovery-search-'));
tempDirs.push(root);
useMatrixHome(path.join(root, '.matrix'));
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/semantic-chat',
version: '1.2.3',
componentType: 'SemanticChatActor',
mount: 'semantic.chat',
});
const manifestPath = path.join(fixture.packageDir, 'matrix.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
manifest.description = 'Semantic package for chat workflows';
manifest.class = 'hybrid';
manifest.tags = ['communication'];
manifest.capabilities = ['conversation'];
manifest.webapp = {
distDir: 'dist',
entry: 'index.html',
appName: 'chat',
displayName: 'Semantic Chat',
icon: '💬',
navOrder: 30,
description: 'Conversation workspace',
shells: ['platform', 'edge'],
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
fs.mkdirSync(path.join(fixture.packageDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(fixture.packageDir, 'dist', 'index.html'), '<!doctype html><title>Semantic Chat</title>', 'utf8');
fs.writeFileSync(
path.join(fixture.packageDir, 'src', 'index.ts'),
`export class SemanticChatActor {
static description = 'Coordinates indexed chat conversations';
static kind = 'service';
static accepts = {
'chat.status': { description: 'Read chat status' },
'chat.send': { description: 'Send a chat message' }
};
static emits = {
'chat.message.created': { description: 'Message created' }
};
static skills = {
'conversation-management': { description: 'Manage conversation state' }
};
static tags = ['chat', 'semantic'];
static capabilities = ['messaging'];
}
`,
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-discovery-search',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
assert.equal(fs.existsSync(path.join(registryDir, 'discovery-index.json')), true);
const searchResult = await runMxCli([
'node',
'mx',
'search',
'conversation',
'--registry-dir',
registryDir,
'--limit',
'5',
'--json',
], { cwd: workspaceDir });
assertExitCode(searchResult, 0);
const payload = parseLastJson(searchResult.logs) as {
query: string;
registry: string;
registrySource: string;
results: Array<{
packageName: string;
version: string;
displayName?: string;
inferredKind?: string;
keywords: readonly string[];
matchedFields?: readonly string[];
}>;
};
assert.equal(payload.query, 'conversation');
assert.equal(payload.registry, registryDir);
assert.equal(payload.registrySource, 'local');
assert.equal(payload.results.length, 1);
assert.equal(payload.results[0]?.packageName, '@acme/semantic-chat');
assert.equal(payload.results[0]?.version, '1.2.3');
assert.equal(payload.results[0]?.displayName, 'Semantic Chat');
assert.equal(payload.results[0]?.inferredKind, 'app');
assert.equal(payload.results[0]?.keywords.includes('conversation-management'), true);
assert.equal(payload.results[0]?.keywords.includes('edge'), true);
assert.equal(payload.results[0]?.matchedFields?.includes('component.skills'), true);
});
});

View File

@ -0,0 +1,120 @@
import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
describe('FP-30 mx update-fork command', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('advances fork originVersion metadata when upstream has a newer release', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-update-fork-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const upstreamV1 = createMatrixPackage(path.join(root, 'upstream-v1'), {
packageName: '@matrix/filesystem',
version: '1.0.0',
componentType: 'FolderProjection',
mount: 'fs',
});
const upstreamV12 = createMatrixPackage(path.join(root, 'upstream-v12'), {
packageName: '@matrix/filesystem',
version: '1.2.0',
componentType: 'FolderProjection',
mount: 'fs',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-update-fork',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
for (const pkg of [upstreamV1, upstreamV12]) {
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: pkg.packageDir });
assertExitCode(publishResult, 0);
}
const installOrigin = await runMxCli([
'node',
'mx',
'install',
'@matrix/filesystem@1.0.0',
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installOrigin, 0);
const forkResult = await runMxCli([
'node',
'mx',
'fork',
'@matrix/filesystem',
'--name',
'@alice/filesystem',
'--packages-dir',
packagesDir,
'--forked-by',
'alice',
'--json',
], { cwd: workspaceDir });
assertExitCode(forkResult, 0);
const updateResult = await runMxCli([
'node',
'mx',
'update-fork',
'@alice/filesystem',
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(updateResult, 0);
const payload = parseLastJson(updateResult.logs) as {
updated: boolean;
previousOriginVersion: string;
nextOriginVersion: string;
};
assert.equal(payload.updated, true);
assert.equal(payload.previousOriginVersion, '1.0.0');
assert.equal(payload.nextOriginVersion, '1.2.0');
});
});

View File

@ -0,0 +1,77 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
export function installOpenMatrixCoreStub(projectDir: string): void {
const pkgDir = path.join(projectDir, 'node_modules', '@open-matrix', 'core');
fs.mkdirSync(pkgDir, { recursive: true });
fs.writeFileSync(
path.join(pkgDir, 'package.json'),
JSON.stringify({
name: '@open-matrix/core',
type: 'module',
types: './index.d.ts',
exports: {
'.': './index.js',
},
}, null, 2) + '\n',
'utf8',
);
fs.writeFileSync(
path.join(pkgDir, 'index.js'),
[
'export class MatrixActor {',
' static accepts = {};',
' static emits = {};',
'}',
'',
'export class MatrixActorHtmlElement extends MatrixActor {}',
'',
'export class InMemoryBroker {}',
'',
'export class InMemoryTransport {',
' constructor(_broker, options = {}) {',
' this.root = options.root || options.name || null;',
' }',
'}',
'',
'export class MatrixRuntime {',
' constructor(options = {}) {',
' this.transport = options.transport || { root: null };',
' }',
' async createSupervised(ActorClass, mount) {',
' const actor = new ActorClass();',
' actor.mount = mount;',
' return actor;',
' }',
'}',
'',
].join('\n'),
'utf8',
);
fs.writeFileSync(
path.join(pkgDir, 'index.d.ts'),
[
'export class MatrixActor {',
' static accepts: Record<string, unknown>;',
' static emits: Record<string, unknown>;',
'}',
'',
'export class MatrixActorHtmlElement extends MatrixActor {}',
'',
'export class InMemoryBroker {}',
'',
'export class InMemoryTransport {',
' readonly root?: string | null;',
' constructor(broker: InMemoryBroker, options?: { name?: string; root?: string });',
'}',
'',
'export class MatrixRuntime {',
' readonly transport: { readonly root?: string | null };',
' constructor(options?: { transport?: InMemoryTransport; logging?: boolean });',
' createSupervised<TActor>(ActorClass: new () => TActor, mount: string): Promise<TActor>;',
'}',
'',
].join('\n'),
'utf8',
);
}

View File

@ -0,0 +1,265 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
loadActorConstructor,
resolveActorRunBroker,
resolvePackageRunSpec,
} from '../../src/commands/actor-run.js';
describe('matrix actor run', () => {
it('loads a named actor export from an entry module', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-entry-'));
try {
const entry = path.join(dir, 'actors.mjs');
fs.writeFileSync(entry, [
'export class NamedActor {}',
'export class OtherActor {}',
'',
].join('\n'), 'utf8');
const actor = await loadActorConstructor(entry, 'NamedActor');
assert.equal(actor.name, 'NamedActor');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('loads a default actor export from an entry module', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-default-entry-'));
try {
const entry = path.join(dir, 'actor.mjs');
fs.writeFileSync(entry, [
'export default class DefaultActor {}',
'',
].join('\n'), 'utf8');
const actor = await loadActorConstructor(entry, 'default');
assert.equal(actor.name, 'DefaultActor');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('reports available actor exports when the requested export is missing', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-missing-entry-'));
try {
const entry = path.join(dir, 'actor.mjs');
fs.writeFileSync(entry, [
'export class PresentActor {}',
'',
].join('\n'), 'utf8');
await assert.rejects(
() => loadActorConstructor(entry, 'MissingActor'),
/No actor class found as export "MissingActor".*PresentActor/u,
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('resolves broker URL, root, JWT, and seed from an explicit home without Host state', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-'));
try {
fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
fs.writeFileSync(
path.join(home, 'credentials', 'matrix-broker.json'),
`${JSON.stringify({
natsUrl: 'nats://broker.example:4222',
root: 'acme-dev',
jwt: 'jwt-home',
seed: 'seed-home',
})}\n`,
'utf8',
);
const resolved = resolveActorRunBroker({ home });
assert.equal(resolved.natsUrl, 'nats://broker.example:4222');
assert.equal(resolved.root, 'acme-dev');
assert.equal(resolved.jwt, 'jwt-home');
assert.equal(resolved.seed, 'seed-home');
assert.ok(resolved.checked.includes(`${home}/credentials/matrix-broker.json`));
assert.equal(fs.existsSync(path.join(home, 'host.json')), false);
assert.equal(fs.existsSync(path.join(home, 'runtimes')), false);
assert.equal(fs.existsSync(path.join(home, 'package-records')), false);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
it('prefers explicit broker options over environment and home configuration', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-precedence-'));
const previous = {
MATRIX_NATS_URL: process.env.MATRIX_NATS_URL,
NATS_URL: process.env.NATS_URL,
MATRIX_ROOT: process.env.MATRIX_ROOT,
MATRIX_SPACE: process.env.MATRIX_SPACE,
MATRIX_NATS_JWT: process.env.MATRIX_NATS_JWT,
MATRIX_NATS_SEED: process.env.MATRIX_NATS_SEED,
};
try {
fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
fs.writeFileSync(
path.join(home, 'credentials', 'matrix-broker.json'),
`${JSON.stringify({
natsUrl: 'nats://home.example:4222',
root: 'home-root',
jwt: 'home-jwt',
seed: 'home-seed',
})}\n`,
'utf8',
);
process.env.MATRIX_NATS_URL = 'nats://env.example:4222';
process.env.MATRIX_ROOT = 'env-root';
process.env.MATRIX_NATS_JWT = 'env-jwt';
process.env.MATRIX_NATS_SEED = 'env-seed';
const resolved = resolveActorRunBroker({
home,
natsUrl: 'nats://explicit.example:4222',
root: 'explicit-root',
jwt: 'explicit-jwt',
seed: 'explicit-seed',
});
assert.equal(resolved.natsUrl, 'nats://explicit.example:4222');
assert.equal(resolved.root, 'explicit-root');
assert.equal(resolved.jwt, 'explicit-jwt');
assert.equal(resolved.seed, 'explicit-seed');
} finally {
restoreEnv(previous);
fs.rmSync(home, { recursive: true, force: true });
}
});
it('uses Space as a root alias for direct broker execution', () => {
const resolved = resolveActorRunBroker({
natsUrl: 'nats://127.0.0.1:4222',
space: 'space-root',
});
assert.equal(resolved.natsUrl, 'nats://127.0.0.1:4222');
assert.equal(resolved.root, 'space-root');
assert.equal(resolved.jwt, undefined);
assert.equal(resolved.seed, undefined);
});
it('reports missing broker URL/root and half credentials before connecting', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-missing-'));
const previous = {
MATRIX_NATS_URL: process.env.MATRIX_NATS_URL,
NATS_URL: process.env.NATS_URL,
MATRIX_ROOT: process.env.MATRIX_ROOT,
MATRIX_SPACE: process.env.MATRIX_SPACE,
MATRIX_NATS_JWT: process.env.MATRIX_NATS_JWT,
MATRIX_NATS_SEED: process.env.MATRIX_NATS_SEED,
NATS_JWT: process.env.NATS_JWT,
NATS_SEED: process.env.NATS_SEED,
};
try {
delete process.env.MATRIX_NATS_URL;
delete process.env.NATS_URL;
delete process.env.MATRIX_ROOT;
delete process.env.MATRIX_SPACE;
delete process.env.MATRIX_NATS_JWT;
delete process.env.MATRIX_NATS_SEED;
delete process.env.NATS_JWT;
delete process.env.NATS_SEED;
assert.throws(
() => resolveActorRunBroker({ home }),
/MATRIX_NATS_URL_REQUIRED/u,
);
assert.throws(
() => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222' }),
/MATRIX_ROOT_REQUIRED/u,
);
assert.throws(
() => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222', root: 'demo', jwt: 'jwt-only' }),
/MATRIX_NATS_JWT_SEED_PAIR_REQUIRED/u,
);
assert.throws(
() => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222', root: 'demo', seed: 'seed-only' }),
/MATRIX_NATS_JWT_SEED_PAIR_REQUIRED/u,
);
} finally {
restoreEnv(previous);
fs.rmSync(home, { recursive: true, force: true });
}
});
it('resolves direct package run metadata from matrix.json runtime.factory without Host state', () => {
const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-package-run-spec-'));
try {
fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'package.json'),
`${JSON.stringify({ name: '@example/direct-run', version: '0.1.0', type: 'module' }, null, 2)}\n`,
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
`${JSON.stringify({
name: '@example/direct-run',
version: '0.1.0',
runtime: {
language: 'javascript',
entry: 'src/index.mjs',
factory: {
kind: 'factory',
export: 'createDirectRunActor',
instance: {
id: 'RUNTIME-HOST-DIRECT-RUN',
class: 'service',
rootKind: 'component',
componentType: 'DirectRunActor',
mount: 'actor-runner-smoke',
autoStart: true,
},
},
},
components: [{
type: 'DirectRunActor',
mount: 'actor-runner-smoke',
accepts: ['ping', 'getStatus'],
}],
}, null, 2)}\n`,
'utf8',
);
fs.writeFileSync(path.join(packageDir, 'src', 'index.mjs'), 'export function createDirectRunActor() {}\n', 'utf8');
const spec = resolvePackageRunSpec(packageDir, { mount: 'actor-runner-smoke' });
assert.equal(spec.packageName, '@example/direct-run');
assert.equal(spec.manifestPath, path.join(packageDir, 'matrix.json'));
assert.equal(spec.exportName, 'createDirectRunActor');
assert.equal(spec.mount, 'actor-runner-smoke');
assert.equal(spec.actorId, 'DirectRunActor');
assert.equal(spec.packageId, '@example/direct-run');
assert.equal(spec.accepts.ping && typeof spec.accepts.ping, 'object');
assert.equal(spec.accepts.getStatus && typeof spec.accepts.getStatus, 'object');
assert.equal(fs.existsSync(path.join(packageDir, 'host.json')), false);
assert.equal(fs.existsSync(path.join(packageDir, 'runtimes')), false);
assert.equal(fs.existsSync(path.join(packageDir, 'package-records')), false);
} finally {
fs.rmSync(packageDir, { recursive: true, force: true });
}
});
});
function restoreEnv(previous: Record<string, string | undefined>): void {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}

View File

@ -0,0 +1,98 @@
/**
* Unit tests for Matrix SDK CLI basic functionality.
*/
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { createProgram } from '../../src/index.js';
describe('Matrix SDK CLI foundation', () => {
it('creates program with correct name and version', () => {
const program = createProgram();
assert.equal(program.name(), 'matrix');
// Version is set via .version() method
const opts = program.opts();
assert.ok(program.commands.length > 0, 'Should have commands registered');
});
it('has init command', () => {
const program = createProgram();
const initCmd = program.commands.find(cmd => cmd.name() === 'init');
assert.ok(initCmd, 'Should have init command');
assert.equal(initCmd.description(), 'Initialize a new Matrix package');
});
it('has actor command', () => {
const program = createProgram();
const actorCmd = program.commands.find(cmd => cmd.name() === 'actor');
assert.ok(actorCmd, 'Should have actor command');
assert.equal(actorCmd.description(), 'Scaffold a new actor package');
assert.equal(actorCmd.options.some(option => option.long === '--dev'), false);
});
it('has install command', () => {
const program = createProgram();
const installCmd = program.commands.find(cmd => cmd.name() === 'install');
assert.ok(installCmd, 'Should have install command');
});
it('has fork command', () => {
const program = createProgram();
const forkCmd = program.commands.find(cmd => cmd.name() === 'fork');
assert.ok(forkCmd, 'Should have fork command');
});
it('has publish command', () => {
const program = createProgram();
const publishCmd = program.commands.find(cmd => cmd.name() === 'publish');
assert.ok(publishCmd, 'Should have publish command');
});
it('has search command', () => {
const program = createProgram();
const searchCmd = program.commands.find(cmd => cmd.name() === 'search');
assert.ok(searchCmd, 'Should have search command');
});
it('has config command', () => {
const program = createProgram();
const configCmd = program.commands.find(cmd => cmd.name() === 'config');
assert.ok(configCmd, 'Should have config command');
const explainCmd = configCmd.commands.find(cmd => cmd.name() === 'explain');
assert.equal(explainCmd, undefined);
});
it('has list command', () => {
const program = createProgram();
const listCmd = program.commands.find(cmd => cmd.name() === 'list');
assert.ok(listCmd, 'Should have list command');
});
it('does not expose managed host commands', () => {
const program = createProgram();
const serviceCmd = program.commands.find(cmd => cmd.name() === 'service');
const runCmd = program.commands.find(cmd => cmd.name() === 'run');
const upCmd = program.commands.find(cmd => cmd.name() === 'up');
assert.equal(serviceCmd, undefined);
assert.equal(runCmd, undefined);
assert.equal(upCmd, undefined);
});
it('has package run command', () => {
const program = createProgram();
const packageCmd = program.commands.find(cmd => cmd.name() === 'package');
assert.ok(packageCmd, 'Should have package command');
const packageRunCmd = packageCmd.commands.find(cmd => cmd.name() === 'run');
assert.ok(packageRunCmd, 'Should have package run command');
assert.equal(packageRunCmd.description(), 'Run one package directly on the selected Space bus without managed host inventory');
});
it('does not expose provider machine-link credential commands', () => {
const program = createProgram();
const refreshCmd = program.commands.find(cmd => cmd.name() === 'refresh-credentials');
const revokeCmd = program.commands.find(cmd => cmd.name() === 'revoke-device');
const linkStatusCmd = program.commands.find(cmd => cmd.name() === 'link-status');
assert.equal(refreshCmd, undefined);
assert.equal(revokeCmd, undefined);
assert.equal(linkStatusCmd, undefined);
});
});

View File

@ -0,0 +1,47 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
configGetCommand,
configListCommand,
configResetCommand,
configSetCommand,
} from '../../src/commands/config.js';
describe('matrix config command', () => {
it('sets, reads, lists, and resets the package registry in a local Matrix config', async () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'matrix-sdk-config-'));
try {
const configPath = path.join(cwd, '.matrix', 'config.json');
const set = await configSetCommand('registry', 'https://registry.example.test/npm', { cwd });
assert.equal(set.configPath, configPath);
assert.equal(set.value, 'https://registry.example.test/npm/');
const get = await configGetCommand('registry', { cwd });
assert.equal(get.value, 'https://registry.example.test/npm/');
assert.equal(get.source, 'config');
const list = await configListCommand({ cwd });
assert.equal(list.values.registry, 'https://registry.example.test/npm/');
assert.equal(list.effective.registry, 'https://registry.example.test/npm/');
assert.equal(list.effective.registrySource, 'config');
const reset = await configResetCommand({ cwd });
assert.equal(reset.configPath, configPath);
assert.equal(fs.existsSync(configPath), false);
} finally {
fs.rmSync(cwd, { recursive: true, force: true });
}
});
it('rejects unknown config keys', async () => {
await assert.rejects(
() => configSetCommand('hostHome', '/var/lib/provider-local'),
/MX_CONFIG_INVALID/u,
);
});
});

Some files were not shown because too many files have changed in this diff Show More