feat(wave2b): migrate @mosaic/mosaic wizard into monorepo
- Installation wizard migrated from mosaic-bootstrap (mosaic-wizard@0.2.0) - Renamed to @mosaic/mosaic@0.1.0 - Wizard-specific types kept in src/types.ts - tsconfig updated to extend monorepo base - Typecheck and build passing
This commit is contained in:
340
packages/mosaic/README.md
Normal file
340
packages/mosaic/README.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# Mosaic Agent Framework
|
||||
|
||||
Universal agent standards layer for Claude Code, Codex, and OpenCode.
|
||||
|
||||
One config, every runtime, same standards.
|
||||
|
||||
> **This repository is a generic framework baseline.** No personal data, credentials, user-specific preferences, or machine-specific paths should be committed. All personalization happens at install time via `mosaic init` or by editing files in `~/.config/mosaic/` after installation.
|
||||
|
||||
## Quick Install
|
||||
|
||||
### Mac / Linux
|
||||
|
||||
```bash
|
||||
curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex
|
||||
```
|
||||
|
||||
### From Source (any platform)
|
||||
|
||||
```bash
|
||||
git clone https://git.mosaicstack.dev/mosaic/bootstrap.git ~/src/mosaic-bootstrap
|
||||
cd ~/src/mosaic-bootstrap && bash install.sh
|
||||
```
|
||||
|
||||
If Node.js 18+ is available, the remote installer automatically uses the TypeScript wizard instead of the bash installer for a richer setup experience.
|
||||
|
||||
The installer will:
|
||||
- Install the framework to `~/.config/mosaic/`
|
||||
- Add `~/.config/mosaic/bin` to your PATH
|
||||
- Sync runtime adapters and skills
|
||||
- Install and configure sequential-thinking MCP (hard requirement)
|
||||
- Run a health audit
|
||||
- Detect existing installs and prompt to keep or overwrite local files
|
||||
- Prompt you to run `mosaic init` to set up your agent identity
|
||||
|
||||
## First Run
|
||||
|
||||
After install, open a new terminal (or `source ~/.bashrc`) and run:
|
||||
|
||||
```bash
|
||||
mosaic init
|
||||
```
|
||||
|
||||
If Node.js 18+ is installed, this launches an interactive wizard with two modes:
|
||||
|
||||
- **Quick Start** (~2 min): agent name + communication style, sensible defaults for everything else
|
||||
- **Advanced**: full customization of identity, user profile, tools, runtimes, and skills
|
||||
|
||||
The wizard configures three files loaded into every agent session:
|
||||
- `SOUL.md` — agent identity contract (name, style, guardrails)
|
||||
- `USER.md` — your user profile (name, timezone, accessibility, preferences)
|
||||
- `TOOLS.md` — machine-level tool reference (git providers, credentials, CLI patterns)
|
||||
|
||||
It also detects installed runtimes (Claude, Codex, OpenCode), configures sequential-thinking MCP, and offers curated skill selection from 8 categories.
|
||||
|
||||
### Non-Interactive Mode
|
||||
|
||||
For CI or scripted installs:
|
||||
|
||||
```bash
|
||||
mosaic init --non-interactive --name Jarvis --style direct --user-name Jason --timezone America/Chicago
|
||||
```
|
||||
|
||||
All flags: `--name`, `--role`, `--style`, `--user-name`, `--pronouns`, `--timezone`, `--mosaic-home`, `--source-dir`.
|
||||
|
||||
### Legacy Fallback
|
||||
|
||||
If Node.js is unavailable, `mosaic init` falls back to the bash-based `mosaic-init` script.
|
||||
|
||||
## Launching Agent Sessions
|
||||
|
||||
```bash
|
||||
mosaic claude # Launch Claude Code with full Mosaic injection
|
||||
mosaic codex # Launch Codex with full Mosaic injection
|
||||
mosaic opencode # Launch OpenCode with full Mosaic injection
|
||||
```
|
||||
|
||||
The launcher:
|
||||
1. Verifies `~/.config/mosaic` exists
|
||||
2. Verifies `SOUL.md` exists (auto-runs `mosaic init` if missing)
|
||||
3. Injects `AGENTS.md` into the runtime
|
||||
4. Forwards all arguments to the runtime CLI
|
||||
|
||||
You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtime adapters will tell the agent to read `~/.config/mosaic/AGENTS.md`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
~/.config/mosaic/
|
||||
├── AGENTS.md ← THE source of truth (all standards, all runtimes)
|
||||
├── SOUL.md ← Agent identity (generated by mosaic init)
|
||||
├── USER.md ← User profile and accessibility (generated by mosaic init)
|
||||
├── TOOLS.md ← Machine-level tool reference (generated by mosaic init)
|
||||
├── STANDARDS.md ← Machine-wide standards
|
||||
├── guides/E2E-DELIVERY.md ← Mandatory E2E software delivery procedure
|
||||
├── guides/PRD.md ← Mandatory PRD requirements gate before coding
|
||||
├── guides/DOCUMENTATION.md ← Mandatory documentation standard and gates
|
||||
├── bin/ ← CLI tools (mosaic, mosaic-init, mosaic-doctor, etc.)
|
||||
├── dist/ ← Bundled wizard (mosaic-wizard.mjs)
|
||||
├── guides/ ← Operational guides
|
||||
├── tools/ ← Tool suites: git, portainer, authentik, coolify, codex, etc.
|
||||
├── runtime/ ← Runtime adapters + runtime-specific references
|
||||
│ ├── claude/CLAUDE.md
|
||||
│ ├── claude/RUNTIME.md
|
||||
│ ├── opencode/AGENTS.md
|
||||
│ ├── opencode/RUNTIME.md
|
||||
│ ├── codex/instructions.md
|
||||
│ ├── codex/RUNTIME.md
|
||||
│ └── mcp/SEQUENTIAL-THINKING.json
|
||||
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
|
||||
├── skills-local/ ← Local cross-runtime skills
|
||||
└── templates/ ← SOUL.md template, project templates
|
||||
```
|
||||
|
||||
### How AGENTS.md Gets Loaded
|
||||
|
||||
| Launch method | Injection mechanism |
|
||||
|--------------|-------------------|
|
||||
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
|
||||
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
|
||||
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
|
||||
| `claude` (direct) | `~/.claude/CLAUDE.md` thin pointer → load AGENTS + runtime reference |
|
||||
| `codex` (direct) | `~/.codex/instructions.md` thin pointer → load AGENTS + runtime reference |
|
||||
| `opencode` (direct) | `~/.config/opencode/AGENTS.md` thin pointer → load AGENTS + runtime reference |
|
||||
|
||||
Mosaic `AGENTS.md` enforces loading `guides/E2E-DELIVERY.md` before execution and
|
||||
requires `guides/PRD.md` before coding and `guides/DOCUMENTATION.md` for code/API/auth/infra documentation gates.
|
||||
|
||||
## Management Commands
|
||||
|
||||
```bash
|
||||
mosaic help # Show all commands
|
||||
mosaic init # Interactive wizard (or legacy init)
|
||||
mosaic doctor # Health audit — detect drift and missing files
|
||||
mosaic sync # Sync skills from canonical source
|
||||
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
|
||||
mosaic upgrade check # Check release upgrade status (no changes)
|
||||
mosaic upgrade # Upgrade installed Mosaic release (keeps SOUL.md by default)
|
||||
mosaic upgrade --dry-run # Preview release upgrade without changes
|
||||
mosaic upgrade --ref main # Upgrade from a specific branch/tag/commit ref
|
||||
mosaic upgrade --overwrite # Upgrade release and overwrite local files
|
||||
mosaic upgrade project ... # Project file cleanup mode (see below)
|
||||
```
|
||||
|
||||
## Upgrading Mosaic Release
|
||||
|
||||
Upgrade the installed framework in place:
|
||||
|
||||
```bash
|
||||
# Default (safe): keep local SOUL.md, USER.md, TOOLS.md + memory
|
||||
mosaic upgrade
|
||||
|
||||
# Check current/target release info without changing files
|
||||
mosaic upgrade check
|
||||
|
||||
# Non-interactive
|
||||
mosaic upgrade --yes
|
||||
|
||||
# Pull a specific ref
|
||||
mosaic upgrade --ref main
|
||||
|
||||
# Force full overwrite (fresh install semantics)
|
||||
mosaic upgrade --overwrite --yes
|
||||
```
|
||||
|
||||
`mosaic upgrade` re-runs the remote installer and passes install mode controls (`keep`/`overwrite`).
|
||||
This is the manual upgrade path today and is suitable for future app-driven update checks.
|
||||
|
||||
## Upgrading Projects
|
||||
|
||||
After centralizing AGENTS.md and SOUL.md, existing projects may have stale files:
|
||||
|
||||
```bash
|
||||
# Preview what would change across all projects
|
||||
mosaic upgrade project --all --dry-run
|
||||
|
||||
# Apply to all projects
|
||||
mosaic upgrade project --all
|
||||
|
||||
# Apply to a specific project
|
||||
mosaic upgrade project ~/src/my-project
|
||||
```
|
||||
|
||||
Backward compatibility is preserved for historical usage:
|
||||
|
||||
```bash
|
||||
mosaic upgrade --all # still routes to project-upgrade
|
||||
mosaic upgrade ~/src/my-repo # still routes to project-upgrade
|
||||
```
|
||||
|
||||
What it does per project:
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `SOUL.md` | Removed — now global at `~/.config/mosaic/SOUL.md` |
|
||||
| `CLAUDE.md` | Replaced with thin pointer to global AGENTS.md |
|
||||
| `AGENTS.md` | Stale load-order sections stripped; project content preserved |
|
||||
|
||||
Backups (`.mosaic-bak`) are created before any modification.
|
||||
|
||||
## Universal Skills
|
||||
|
||||
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories (`~/.claude/skills`, `~/.codex/skills`, `~/.config/opencode/skills`).
|
||||
|
||||
```bash
|
||||
mosaic sync # Full sync (clone + link)
|
||||
~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only
|
||||
```
|
||||
|
||||
## Runtime Compatibility
|
||||
|
||||
The installer pushes thin runtime adapters as regular files (not symlinks):
|
||||
|
||||
- `~/.claude/CLAUDE.md` — pointer to `~/.config/mosaic/AGENTS.md`
|
||||
- `~/.claude/settings.json`, `hooks-config.json`, `context7-integration.md`
|
||||
- `~/.config/opencode/AGENTS.md` — pointer to `~/.config/mosaic/AGENTS.md`
|
||||
- `~/.codex/instructions.md` — pointer to `~/.config/mosaic/AGENTS.md`
|
||||
- `~/.claude/settings.json`, `~/.codex/config.toml`, and `~/.config/opencode/config.json` include sequential-thinking MCP config
|
||||
|
||||
Re-sync manually:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-link-runtime-assets
|
||||
```
|
||||
|
||||
## MCP Registration
|
||||
|
||||
### How MCPs Are Configured in Claude Code
|
||||
|
||||
**MCPs must be registered via `claude mcp add` — not by hand-editing `~/.claude/settings.json`.**
|
||||
|
||||
`settings.json` controls hooks, model, plugins, and allowed commands. The `mcpServers` key in
|
||||
`settings.json` is silently ignored by Claude Code's MCP loader. The correct file is `~/.claude.json`,
|
||||
which is managed by the `claude mcp` CLI.
|
||||
|
||||
```bash
|
||||
# Register a stdio MCP (user scope = all projects, persists across sessions)
|
||||
claude mcp add --scope user <name> -- npx -y <package>
|
||||
|
||||
# Register an HTTP MCP (e.g. OpenBrain)
|
||||
claude mcp add --scope user --transport http <name> <url> \
|
||||
--header "Authorization: Bearer <token>"
|
||||
|
||||
# List registered MCPs
|
||||
claude mcp list
|
||||
```
|
||||
|
||||
**Scope options:**
|
||||
- `--scope user` — writes to `~/.claude.json`, available in all projects (recommended for shared tools)
|
||||
- `--scope project` — writes to `.claude/settings.json` in the project root, committed to the repo
|
||||
- `--scope local` — default, machine-local only, not committed
|
||||
|
||||
**Transport for HTTP MCPs must be `http`** — not `sse`. `type: "sse"` is a deprecated protocol
|
||||
that silently fails to connect against FastMCP streamable HTTP servers.
|
||||
|
||||
### sequential-thinking MCP (Hard Requirement)
|
||||
|
||||
sequential-thinking MCP is required for Mosaic Stack. The installer registers it automatically.
|
||||
To verify or re-register manually:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking
|
||||
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
|
||||
```
|
||||
|
||||
### OpenBrain Semantic Memory (Recommended)
|
||||
|
||||
OpenBrain is the shared cross-agent memory layer. Register once per machine:
|
||||
|
||||
```bash
|
||||
claude mcp add --scope user --transport http openbrain https://your-openbrain-host/mcp \
|
||||
--header "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
See [mosaic/openbrain](https://git.mosaicstack.dev/mosaic/openbrain) for setup and API docs.
|
||||
|
||||
## Bootstrap Any Repo
|
||||
|
||||
Attach any repository to the Mosaic standards layer:
|
||||
|
||||
```bash
|
||||
mosaic bootstrap /path/to/repo
|
||||
```
|
||||
|
||||
This creates `.mosaic/`, `scripts/agent/`, and an `AGENTS.md` if missing.
|
||||
|
||||
## Quality Rails
|
||||
|
||||
Apply and verify quality templates:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-quality-apply --template typescript-node --target /path/to/repo
|
||||
~/.config/mosaic/bin/mosaic-quality-verify --target /path/to/repo
|
||||
```
|
||||
|
||||
Templates: `typescript-node`, `typescript-nextjs`, `monorepo`
|
||||
|
||||
## Health Audit
|
||||
|
||||
```bash
|
||||
mosaic doctor # Standard audit
|
||||
~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode
|
||||
```
|
||||
|
||||
## Wizard Development
|
||||
|
||||
The installation wizard is a TypeScript project in the root of this repo.
|
||||
|
||||
```bash
|
||||
pnpm install # Install dependencies
|
||||
pnpm dev # Run wizard from source (tsx)
|
||||
pnpm build # Bundle to dist/mosaic-wizard.mjs
|
||||
pnpm test # Run tests (30 tests, vitest)
|
||||
pnpm typecheck # TypeScript type checking
|
||||
```
|
||||
|
||||
The wizard uses `@clack/prompts` for the interactive TUI and supports `--non-interactive` mode via `HeadlessPrompter` for CI and scripted installs. The bundled output (`dist/mosaic-wizard.mjs`) is committed to the repo so installs work without `node_modules`.
|
||||
|
||||
## Re-installing / Updating
|
||||
|
||||
Pull the latest and re-run the installer:
|
||||
|
||||
```bash
|
||||
cd ~/src/mosaic-bootstrap && git pull && bash install.sh
|
||||
```
|
||||
|
||||
If an existing install is detected, the installer prompts for:
|
||||
- `keep` (recommended): preserve local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/`
|
||||
- `overwrite`: replace everything in `~/.config/mosaic`
|
||||
|
||||
Or use the one-liner again — it always pulls the latest:
|
||||
|
||||
```bash
|
||||
curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
||||
```
|
||||
39
packages/mosaic/package.json
Normal file
39
packages/mosaic/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@mosaic/mosaic",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Mosaic installation wizard and meta-package entry point",
|
||||
"bin": {
|
||||
"mosaic-wizard": "./dist/mosaic-wizard.mjs"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"test": "vitest run",
|
||||
"dev": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.9",
|
||||
"commander": "^13",
|
||||
"picocolors": "^1.1",
|
||||
"yaml": "^2.7",
|
||||
"zod": "^3.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"tsdown": "^0.12",
|
||||
"tsx": "^4",
|
||||
"typescript": "^5",
|
||||
"vitest": "^2"
|
||||
}
|
||||
}
|
||||
26
packages/mosaic/src/config/config-service.ts
Normal file
26
packages/mosaic/src/config/config-service.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { SoulConfig, UserConfig, ToolsConfig, InstallAction } from '../types.js';
|
||||
import { FileConfigAdapter } from './file-adapter.js';
|
||||
|
||||
/**
|
||||
* ConfigService interface — abstracts config read/write operations.
|
||||
* Currently backed by FileConfigAdapter (writes .md files from templates).
|
||||
* Designed for future swap to SqliteConfigAdapter or PostgresConfigAdapter.
|
||||
*/
|
||||
export interface ConfigService {
|
||||
readSoul(): Promise<SoulConfig>;
|
||||
readUser(): Promise<UserConfig>;
|
||||
readTools(): Promise<ToolsConfig>;
|
||||
|
||||
writeSoul(config: SoulConfig): Promise<void>;
|
||||
writeUser(config: UserConfig): Promise<void>;
|
||||
writeTools(config: ToolsConfig): Promise<void>;
|
||||
|
||||
syncFramework(action: InstallAction): Promise<void>;
|
||||
}
|
||||
|
||||
export function createConfigService(
|
||||
mosaicHome: string,
|
||||
sourceDir: string,
|
||||
): ConfigService {
|
||||
return new FileConfigAdapter(mosaicHome, sourceDir);
|
||||
}
|
||||
163
packages/mosaic/src/config/file-adapter.ts
Normal file
163
packages/mosaic/src/config/file-adapter.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { ConfigService } from './config-service.js';
|
||||
import type {
|
||||
SoulConfig,
|
||||
UserConfig,
|
||||
ToolsConfig,
|
||||
InstallAction,
|
||||
} from '../types.js';
|
||||
import { soulSchema, userSchema, toolsSchema } from './schemas.js';
|
||||
import { renderTemplate } from '../template/engine.js';
|
||||
import {
|
||||
buildSoulTemplateVars,
|
||||
buildUserTemplateVars,
|
||||
buildToolsTemplateVars,
|
||||
} from '../template/builders.js';
|
||||
import { atomicWrite, backupFile, syncDirectory } from '../platform/file-ops.js';
|
||||
|
||||
/**
|
||||
* Parse a SoulConfig from an existing SOUL.md file.
|
||||
*/
|
||||
function parseSoulFromMarkdown(content: string): SoulConfig {
|
||||
const config: SoulConfig = {};
|
||||
|
||||
const nameMatch = content.match(/You are \*\*(.+?)\*\*/);
|
||||
if (nameMatch) config.agentName = nameMatch[1];
|
||||
|
||||
const roleMatch = content.match(/Role identity: (.+)/);
|
||||
if (roleMatch) config.roleDescription = roleMatch[1];
|
||||
|
||||
if (content.includes('Be direct, concise')) {
|
||||
config.communicationStyle = 'direct';
|
||||
} else if (content.includes('Be warm and conversational')) {
|
||||
config.communicationStyle = 'friendly';
|
||||
} else if (content.includes('Use professional, structured')) {
|
||||
config.communicationStyle = 'formal';
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a UserConfig from an existing USER.md file.
|
||||
*/
|
||||
function parseUserFromMarkdown(content: string): UserConfig {
|
||||
const config: UserConfig = {};
|
||||
|
||||
const nameMatch = content.match(/\*\*Name:\*\* (.+)/);
|
||||
if (nameMatch) config.userName = nameMatch[1];
|
||||
|
||||
const pronounsMatch = content.match(/\*\*Pronouns:\*\* (.+)/);
|
||||
if (pronounsMatch) config.pronouns = pronounsMatch[1];
|
||||
|
||||
const tzMatch = content.match(/\*\*Timezone:\*\* (.+)/);
|
||||
if (tzMatch) config.timezone = tzMatch[1];
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a ToolsConfig from an existing TOOLS.md file.
|
||||
*/
|
||||
function parseToolsFromMarkdown(content: string): ToolsConfig {
|
||||
const config: ToolsConfig = {};
|
||||
|
||||
const credsMatch = content.match(/\*\*Location:\*\* (.+)/);
|
||||
if (credsMatch) config.credentialsLocation = credsMatch[1];
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export class FileConfigAdapter implements ConfigService {
|
||||
constructor(
|
||||
private mosaicHome: string,
|
||||
private sourceDir: string,
|
||||
) {}
|
||||
|
||||
async readSoul(): Promise<SoulConfig> {
|
||||
const path = join(this.mosaicHome, 'SOUL.md');
|
||||
if (!existsSync(path)) return {};
|
||||
return parseSoulFromMarkdown(readFileSync(path, 'utf-8'));
|
||||
}
|
||||
|
||||
async readUser(): Promise<UserConfig> {
|
||||
const path = join(this.mosaicHome, 'USER.md');
|
||||
if (!existsSync(path)) return {};
|
||||
return parseUserFromMarkdown(readFileSync(path, 'utf-8'));
|
||||
}
|
||||
|
||||
async readTools(): Promise<ToolsConfig> {
|
||||
const path = join(this.mosaicHome, 'TOOLS.md');
|
||||
if (!existsSync(path)) return {};
|
||||
return parseToolsFromMarkdown(readFileSync(path, 'utf-8'));
|
||||
}
|
||||
|
||||
async writeSoul(config: SoulConfig): Promise<void> {
|
||||
const validated = soulSchema.parse(config);
|
||||
const templatePath = this.findTemplate('SOUL.md.template');
|
||||
if (!templatePath) return;
|
||||
|
||||
const template = readFileSync(templatePath, 'utf-8');
|
||||
const vars = buildSoulTemplateVars(validated);
|
||||
const output = renderTemplate(template, vars);
|
||||
|
||||
const outPath = join(this.mosaicHome, 'SOUL.md');
|
||||
backupFile(outPath);
|
||||
atomicWrite(outPath, output);
|
||||
}
|
||||
|
||||
async writeUser(config: UserConfig): Promise<void> {
|
||||
const validated = userSchema.parse(config);
|
||||
const templatePath = this.findTemplate('USER.md.template');
|
||||
if (!templatePath) return;
|
||||
|
||||
const template = readFileSync(templatePath, 'utf-8');
|
||||
const vars = buildUserTemplateVars(validated);
|
||||
const output = renderTemplate(template, vars);
|
||||
|
||||
const outPath = join(this.mosaicHome, 'USER.md');
|
||||
backupFile(outPath);
|
||||
atomicWrite(outPath, output);
|
||||
}
|
||||
|
||||
async writeTools(config: ToolsConfig): Promise<void> {
|
||||
const validated = toolsSchema.parse(config);
|
||||
const templatePath = this.findTemplate('TOOLS.md.template');
|
||||
if (!templatePath) return;
|
||||
|
||||
const template = readFileSync(templatePath, 'utf-8');
|
||||
const vars = buildToolsTemplateVars(validated);
|
||||
const output = renderTemplate(template, vars);
|
||||
|
||||
const outPath = join(this.mosaicHome, 'TOOLS.md');
|
||||
backupFile(outPath);
|
||||
atomicWrite(outPath, output);
|
||||
}
|
||||
|
||||
async syncFramework(action: InstallAction): Promise<void> {
|
||||
const preservePaths =
|
||||
action === 'keep' || action === 'reconfigure'
|
||||
? ['SOUL.md', 'USER.md', 'TOOLS.md', 'memory']
|
||||
: [];
|
||||
|
||||
syncDirectory(this.sourceDir, this.mosaicHome, {
|
||||
preserve: preservePaths,
|
||||
excludeGit: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for template in source dir first, then mosaic home.
|
||||
*/
|
||||
private findTemplate(name: string): string | null {
|
||||
const candidates = [
|
||||
join(this.sourceDir, 'templates', name),
|
||||
join(this.mosaicHome, 'templates', name),
|
||||
];
|
||||
for (const path of candidates) {
|
||||
if (existsSync(path)) return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
51
packages/mosaic/src/config/schemas.ts
Normal file
51
packages/mosaic/src/config/schemas.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const communicationStyleSchema = z
|
||||
.enum(['direct', 'friendly', 'formal'])
|
||||
.default('direct');
|
||||
|
||||
export const soulSchema = z
|
||||
.object({
|
||||
agentName: z.string().min(1).max(50).default('Assistant'),
|
||||
roleDescription: z
|
||||
.string()
|
||||
.default('execution partner and visibility engine'),
|
||||
communicationStyle: communicationStyleSchema,
|
||||
accessibility: z.string().default('none'),
|
||||
customGuardrails: z.string().default(''),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const gitProviderSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().min(1),
|
||||
cli: z.string().min(1),
|
||||
purpose: z.string().min(1),
|
||||
});
|
||||
|
||||
export const userSchema = z
|
||||
.object({
|
||||
userName: z.string().default(''),
|
||||
pronouns: z.string().default('They/Them'),
|
||||
timezone: z.string().default('UTC'),
|
||||
background: z.string().default('(not configured)'),
|
||||
accessibilitySection: z
|
||||
.string()
|
||||
.default(
|
||||
'(No specific accommodations configured. Edit this section to add any.)',
|
||||
),
|
||||
communicationPrefs: z.string().default(''),
|
||||
personalBoundaries: z
|
||||
.string()
|
||||
.default('(Edit this section to add any personal boundaries.)'),
|
||||
projectsTable: z.string().default(''),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const toolsSchema = z
|
||||
.object({
|
||||
gitProviders: z.array(gitProviderSchema).default([]),
|
||||
credentialsLocation: z.string().default('none'),
|
||||
customToolsSection: z.string().default(''),
|
||||
})
|
||||
.partial();
|
||||
38
packages/mosaic/src/constants.ts
Normal file
38
packages/mosaic/src/constants.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const VERSION = '0.2.0';
|
||||
|
||||
export const DEFAULT_MOSAIC_HOME = join(homedir(), '.config', 'mosaic');
|
||||
|
||||
export const DEFAULTS = {
|
||||
agentName: 'Assistant',
|
||||
roleDescription: 'execution partner and visibility engine',
|
||||
communicationStyle: 'direct' as const,
|
||||
pronouns: 'They/Them',
|
||||
timezone: 'UTC',
|
||||
background: '(not configured)',
|
||||
accessibilitySection: '(No specific accommodations configured. Edit this section to add any.)',
|
||||
personalBoundaries: '(Edit this section to add any personal boundaries.)',
|
||||
projectsTable: `| Project | Stack | Registry |
|
||||
|---------|-------|----------|
|
||||
| (none configured) | | |`,
|
||||
credentialsLocation: 'none',
|
||||
customToolsSection: `## Custom Tools
|
||||
|
||||
(Add any machine-specific tools, scripts, or workflows here.)`,
|
||||
gitProvidersTable: `| Instance | URL | CLI | Purpose |
|
||||
|----------|-----|-----|---------|
|
||||
| (add your git providers here) | | | |`,
|
||||
};
|
||||
|
||||
export const RECOMMENDED_SKILLS = new Set([
|
||||
'brainstorming',
|
||||
'code-review-excellence',
|
||||
'lint',
|
||||
'systematic-debugging',
|
||||
'verification-before-completion',
|
||||
'writing-plans',
|
||||
'executing-plans',
|
||||
'architecture-patterns',
|
||||
]);
|
||||
20
packages/mosaic/src/errors.ts
Normal file
20
packages/mosaic/src/errors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export class WizardCancelledError extends Error {
|
||||
override name = 'WizardCancelledError';
|
||||
constructor() {
|
||||
super('Wizard cancelled by user');
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
override name = 'ValidationError';
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class TemplateError extends Error {
|
||||
override name = 'TemplateError';
|
||||
constructor(templatePath: string, message: string) {
|
||||
super(`Template error in ${templatePath}: ${message}`);
|
||||
}
|
||||
}
|
||||
81
packages/mosaic/src/index.ts
Normal file
81
packages/mosaic/src/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Command } from 'commander';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { ClackPrompter } from './prompter/clack-prompter.js';
|
||||
import { HeadlessPrompter } from './prompter/headless-prompter.js';
|
||||
import { createConfigService } from './config/config-service.js';
|
||||
import { runWizard } from './wizard.js';
|
||||
import { WizardCancelledError } from './errors.js';
|
||||
import { VERSION, DEFAULT_MOSAIC_HOME } from './constants.js';
|
||||
import type { CommunicationStyle } from './types.js';
|
||||
|
||||
const program = new Command()
|
||||
.name('mosaic-wizard')
|
||||
.description('Mosaic Installation Wizard')
|
||||
.version(VERSION);
|
||||
|
||||
program
|
||||
.option('--non-interactive', 'Run without prompts (uses defaults + flags)')
|
||||
.option(
|
||||
'--source-dir <path>',
|
||||
'Source directory for framework files',
|
||||
)
|
||||
.option(
|
||||
'--mosaic-home <path>',
|
||||
'Target config directory',
|
||||
DEFAULT_MOSAIC_HOME,
|
||||
)
|
||||
// SOUL.md overrides
|
||||
.option('--name <name>', 'Agent name')
|
||||
.option('--role <description>', 'Agent role description')
|
||||
.option('--style <style>', 'Communication style: direct|friendly|formal')
|
||||
.option('--accessibility <prefs>', 'Accessibility preferences')
|
||||
.option('--guardrails <rules>', 'Custom guardrails')
|
||||
// USER.md overrides
|
||||
.option('--user-name <name>', 'Your name')
|
||||
.option('--pronouns <pronouns>', 'Your pronouns')
|
||||
.option('--timezone <tz>', 'Your timezone')
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const mosaicHome: string = opts.mosaicHome;
|
||||
const sourceDir: string = opts.sourceDir ?? mosaicHome;
|
||||
|
||||
const prompter = opts.nonInteractive
|
||||
? new HeadlessPrompter()
|
||||
: new ClackPrompter();
|
||||
|
||||
const configService = createConfigService(mosaicHome, sourceDir);
|
||||
|
||||
const style = opts.style as CommunicationStyle | undefined;
|
||||
|
||||
await runWizard({
|
||||
mosaicHome,
|
||||
sourceDir,
|
||||
prompter,
|
||||
configService,
|
||||
cliOverrides: {
|
||||
soul: {
|
||||
agentName: opts.name,
|
||||
roleDescription: opts.role,
|
||||
communicationStyle: style,
|
||||
accessibility: opts.accessibility,
|
||||
customGuardrails: opts.guardrails,
|
||||
},
|
||||
user: {
|
||||
userName: opts.userName,
|
||||
pronouns: opts.pronouns,
|
||||
timezone: opts.timezone,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof WizardCancelledError) {
|
||||
console.log('\nWizard cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
console.error('Wizard failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
44
packages/mosaic/src/platform/detect.ts
Normal file
44
packages/mosaic/src/platform/detect.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir, platform } from 'node:os';
|
||||
|
||||
export type ShellType = 'zsh' | 'bash' | 'fish' | 'powershell' | 'unknown';
|
||||
|
||||
export function detectShell(): ShellType {
|
||||
const shell = process.env.SHELL ?? '';
|
||||
if (shell.includes('zsh')) return 'zsh';
|
||||
if (shell.includes('bash')) return 'bash';
|
||||
if (shell.includes('fish')) return 'fish';
|
||||
if (platform() === 'win32') return 'powershell';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export function getShellProfilePath(): string | null {
|
||||
const home = homedir();
|
||||
|
||||
if (platform() === 'win32') {
|
||||
return join(
|
||||
home,
|
||||
'Documents',
|
||||
'PowerShell',
|
||||
'Microsoft.PowerShell_profile.ps1',
|
||||
);
|
||||
}
|
||||
|
||||
const shell = detectShell();
|
||||
switch (shell) {
|
||||
case 'zsh': {
|
||||
const zdotdir = process.env.ZDOTDIR ?? home;
|
||||
return join(zdotdir, '.zshrc');
|
||||
}
|
||||
case 'bash': {
|
||||
const bashrc = join(home, '.bashrc');
|
||||
if (existsSync(bashrc)) return bashrc;
|
||||
return join(home, '.profile');
|
||||
}
|
||||
case 'fish':
|
||||
return join(home, '.config', 'fish', 'config.fish');
|
||||
default:
|
||||
return join(home, '.profile');
|
||||
}
|
||||
}
|
||||
116
packages/mosaic/src/platform/file-ops.ts
Normal file
116
packages/mosaic/src/platform/file-ops.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
copyFileSync,
|
||||
renameSync,
|
||||
readdirSync,
|
||||
unlinkSync,
|
||||
cpSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
|
||||
const MAX_BACKUPS = 3;
|
||||
|
||||
/**
|
||||
* Atomic write: write to temp file, then rename.
|
||||
* Creates parent directories as needed.
|
||||
*/
|
||||
export function atomicWrite(filePath: string, content: string): void {
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
||||
writeFileSync(tmpPath, content, 'utf-8');
|
||||
renameSync(tmpPath, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup of a file before overwriting.
|
||||
* Rotates backups to keep at most MAX_BACKUPS.
|
||||
*/
|
||||
export function backupFile(filePath: string): string | null {
|
||||
if (!existsSync(filePath)) return null;
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, '')
|
||||
.replace('T', '-')
|
||||
.slice(0, 19);
|
||||
const backupPath = `${filePath}.bak-${timestamp}`;
|
||||
copyFileSync(filePath, backupPath);
|
||||
rotateBackups(filePath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
function rotateBackups(filePath: string): void {
|
||||
const dir = dirname(filePath);
|
||||
const baseName = filePath.split('/').pop()!;
|
||||
const prefix = `${baseName}.bak-`;
|
||||
|
||||
try {
|
||||
const backups = readdirSync(dir)
|
||||
.filter((f) => f.startsWith(prefix))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
for (let i = MAX_BACKUPS; i < backups.length; i++) {
|
||||
unlinkSync(join(dir, backups[i]));
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: backup rotation failure doesn't block writes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a source directory to a target, with optional preserve paths.
|
||||
* Replaces the rsync/cp logic from install.sh.
|
||||
*/
|
||||
export function syncDirectory(
|
||||
source: string,
|
||||
target: string,
|
||||
options: { preserve?: string[]; excludeGit?: boolean } = {},
|
||||
): void {
|
||||
const preserveSet = new Set(options.preserve ?? []);
|
||||
|
||||
// Collect files from source
|
||||
function copyRecursive(src: string, dest: string, relBase: string): void {
|
||||
if (!existsSync(src)) return;
|
||||
|
||||
const stat = statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
const relPath = relative(relBase, src);
|
||||
|
||||
// Skip .git
|
||||
if (options.excludeGit && relPath === '.git') return;
|
||||
|
||||
// Skip preserved paths at top level
|
||||
if (preserveSet.has(relPath) && existsSync(dest)) return;
|
||||
|
||||
mkdirSync(dest, { recursive: true });
|
||||
for (const entry of readdirSync(src)) {
|
||||
copyRecursive(join(src, entry), join(dest, entry), relBase);
|
||||
}
|
||||
} else {
|
||||
const relPath = relative(relBase, src);
|
||||
|
||||
// Skip preserved files at top level
|
||||
if (preserveSet.has(relPath) && existsSync(dest)) return;
|
||||
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
copyRecursive(source, target, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely read a file, returning null if it doesn't exist.
|
||||
*/
|
||||
export function safeReadFile(filePath: string): string | null {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
157
packages/mosaic/src/prompter/clack-prompter.ts
Normal file
157
packages/mosaic/src/prompter/clack-prompter.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import * as p from '@clack/prompts';
|
||||
import { WizardCancelledError } from '../errors.js';
|
||||
import type {
|
||||
WizardPrompter,
|
||||
SelectOption,
|
||||
MultiSelectOption,
|
||||
ProgressHandle,
|
||||
} from './interface.js';
|
||||
|
||||
function guardCancel<T>(value: T | symbol): T {
|
||||
if (p.isCancel(value)) {
|
||||
throw new WizardCancelledError();
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export class ClackPrompter implements WizardPrompter {
|
||||
intro(message: string): void {
|
||||
p.intro(message);
|
||||
}
|
||||
|
||||
outro(message: string): void {
|
||||
p.outro(message);
|
||||
}
|
||||
|
||||
note(message: string, title?: string): void {
|
||||
p.note(message, title);
|
||||
}
|
||||
|
||||
log(message: string): void {
|
||||
p.log.info(message);
|
||||
}
|
||||
|
||||
warn(message: string): void {
|
||||
p.log.warn(message);
|
||||
}
|
||||
|
||||
async text(opts: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
validate?: (value: string) => string | void;
|
||||
}): Promise<string> {
|
||||
const validate = opts.validate
|
||||
? (v: string) => {
|
||||
const r = opts.validate!(v);
|
||||
return r === undefined ? undefined : r;
|
||||
}
|
||||
: undefined;
|
||||
const result = await p.text({
|
||||
message: opts.message,
|
||||
placeholder: opts.placeholder,
|
||||
defaultValue: opts.defaultValue,
|
||||
validate,
|
||||
});
|
||||
return guardCancel(result);
|
||||
}
|
||||
|
||||
async confirm(opts: {
|
||||
message: string;
|
||||
initialValue?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const result = await p.confirm({
|
||||
message: opts.message,
|
||||
initialValue: opts.initialValue,
|
||||
});
|
||||
return guardCancel(result);
|
||||
}
|
||||
|
||||
async select<T>(opts: {
|
||||
message: string;
|
||||
options: SelectOption<T>[];
|
||||
initialValue?: T;
|
||||
}): Promise<T> {
|
||||
const clackOptions = opts.options.map((o) => ({
|
||||
value: o.value as T,
|
||||
label: o.label,
|
||||
hint: o.hint,
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- clack Option conditional type needs concrete Primitive
|
||||
const result = await p.select({
|
||||
message: opts.message,
|
||||
options: clackOptions as any,
|
||||
initialValue: opts.initialValue,
|
||||
});
|
||||
return guardCancel(result) as T;
|
||||
}
|
||||
|
||||
async multiselect<T>(opts: {
|
||||
message: string;
|
||||
options: MultiSelectOption<T>[];
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const clackOptions = opts.options.map((o) => ({
|
||||
value: o.value as T,
|
||||
label: o.label,
|
||||
hint: o.hint,
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await p.multiselect({
|
||||
message: opts.message,
|
||||
options: clackOptions as any,
|
||||
required: opts.required,
|
||||
initialValues: opts.options
|
||||
.filter((o) => o.selected)
|
||||
.map((o) => o.value),
|
||||
});
|
||||
return guardCancel(result) as T[];
|
||||
}
|
||||
|
||||
async groupMultiselect<T>(opts: {
|
||||
message: string;
|
||||
options: Record<string, MultiSelectOption<T>[]>;
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const grouped: Record<string, { value: T; label: string; hint?: string }[]> = {};
|
||||
for (const [group, items] of Object.entries(opts.options)) {
|
||||
grouped[group] = items.map((o) => ({
|
||||
value: o.value as T,
|
||||
label: o.label,
|
||||
hint: o.hint,
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await p.groupMultiselect({
|
||||
message: opts.message,
|
||||
options: grouped as any,
|
||||
required: opts.required,
|
||||
});
|
||||
return guardCancel(result) as T[];
|
||||
}
|
||||
|
||||
spinner(): ProgressHandle {
|
||||
const s = p.spinner();
|
||||
let started = false;
|
||||
return {
|
||||
update(message: string) {
|
||||
if (!started) {
|
||||
s.start(message);
|
||||
started = true;
|
||||
} else {
|
||||
s.message(message);
|
||||
}
|
||||
},
|
||||
stop(message?: string) {
|
||||
if (started) {
|
||||
s.stop(message);
|
||||
started = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
separator(): void {
|
||||
p.log.info('');
|
||||
}
|
||||
}
|
||||
133
packages/mosaic/src/prompter/headless-prompter.ts
Normal file
133
packages/mosaic/src/prompter/headless-prompter.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
WizardPrompter,
|
||||
SelectOption,
|
||||
MultiSelectOption,
|
||||
ProgressHandle,
|
||||
} from './interface.js';
|
||||
|
||||
export type AnswerValue = string | boolean | string[];
|
||||
|
||||
export class HeadlessPrompter implements WizardPrompter {
|
||||
private answers: Map<string, AnswerValue>;
|
||||
private logs: string[] = [];
|
||||
|
||||
constructor(answers: Record<string, AnswerValue> = {}) {
|
||||
this.answers = new Map(Object.entries(answers));
|
||||
}
|
||||
|
||||
intro(message: string): void {
|
||||
this.logs.push(`[intro] ${message}`);
|
||||
}
|
||||
outro(message: string): void {
|
||||
this.logs.push(`[outro] ${message}`);
|
||||
}
|
||||
note(message: string, title?: string): void {
|
||||
this.logs.push(`[note] ${title ?? ''}: ${message}`);
|
||||
}
|
||||
log(message: string): void {
|
||||
this.logs.push(`[log] ${message}`);
|
||||
}
|
||||
warn(message: string): void {
|
||||
this.logs.push(`[warn] ${message}`);
|
||||
}
|
||||
|
||||
async text(opts: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
validate?: (value: string) => string | void;
|
||||
}): Promise<string> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
const value =
|
||||
typeof answer === 'string'
|
||||
? answer
|
||||
: opts.defaultValue !== undefined
|
||||
? opts.defaultValue
|
||||
: undefined;
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`HeadlessPrompter: no answer for "${opts.message}"`);
|
||||
}
|
||||
|
||||
if (opts.validate) {
|
||||
const error = opts.validate(value);
|
||||
if (error) throw new Error(`HeadlessPrompter validation failed for "${opts.message}": ${error}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async confirm(opts: {
|
||||
message: string;
|
||||
initialValue?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (typeof answer === 'boolean') return answer;
|
||||
return opts.initialValue ?? true;
|
||||
}
|
||||
|
||||
async select<T>(opts: {
|
||||
message: string;
|
||||
options: SelectOption<T>[];
|
||||
initialValue?: T;
|
||||
}): Promise<T> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (answer !== undefined) {
|
||||
// Find matching option by value string comparison
|
||||
const match = opts.options.find(
|
||||
(o) => String(o.value) === String(answer),
|
||||
);
|
||||
if (match) return match.value;
|
||||
}
|
||||
if (opts.initialValue !== undefined) return opts.initialValue;
|
||||
if (opts.options.length === 0) {
|
||||
throw new Error(`HeadlessPrompter: no options for "${opts.message}"`);
|
||||
}
|
||||
return opts.options[0].value;
|
||||
}
|
||||
|
||||
async multiselect<T>(opts: {
|
||||
message: string;
|
||||
options: MultiSelectOption<T>[];
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (Array.isArray(answer)) {
|
||||
return opts.options
|
||||
.filter((o) => answer.includes(String(o.value)))
|
||||
.map((o) => o.value);
|
||||
}
|
||||
return opts.options.filter((o) => o.selected).map((o) => o.value);
|
||||
}
|
||||
|
||||
async groupMultiselect<T>(opts: {
|
||||
message: string;
|
||||
options: Record<string, MultiSelectOption<T>[]>;
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (Array.isArray(answer)) {
|
||||
const all = Object.values(opts.options).flat();
|
||||
return all
|
||||
.filter((o) => answer.includes(String(o.value)))
|
||||
.map((o) => o.value);
|
||||
}
|
||||
return Object.values(opts.options)
|
||||
.flat()
|
||||
.filter((o) => o.selected)
|
||||
.map((o) => o.value);
|
||||
}
|
||||
|
||||
spinner(): ProgressHandle {
|
||||
return {
|
||||
update(_message: string) {},
|
||||
stop(_message?: string) {},
|
||||
};
|
||||
}
|
||||
|
||||
separator(): void {}
|
||||
|
||||
getLogs(): string[] {
|
||||
return [...this.logs];
|
||||
}
|
||||
}
|
||||
56
packages/mosaic/src/prompter/interface.ts
Normal file
56
packages/mosaic/src/prompter/interface.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface SelectOption<T = string> {
|
||||
value: T;
|
||||
label: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export interface MultiSelectOption<T = string> extends SelectOption<T> {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressHandle {
|
||||
update(message: string): void;
|
||||
stop(message?: string): void;
|
||||
}
|
||||
|
||||
export interface WizardPrompter {
|
||||
intro(message: string): void;
|
||||
outro(message: string): void;
|
||||
note(message: string, title?: string): void;
|
||||
log(message: string): void;
|
||||
warn(message: string): void;
|
||||
|
||||
text(opts: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
validate?: (value: string) => string | void;
|
||||
}): Promise<string>;
|
||||
|
||||
confirm(opts: {
|
||||
message: string;
|
||||
initialValue?: boolean;
|
||||
}): Promise<boolean>;
|
||||
|
||||
select<T>(opts: {
|
||||
message: string;
|
||||
options: SelectOption<T>[];
|
||||
initialValue?: T;
|
||||
}): Promise<T>;
|
||||
|
||||
multiselect<T>(opts: {
|
||||
message: string;
|
||||
options: MultiSelectOption<T>[];
|
||||
required?: boolean;
|
||||
}): Promise<T[]>;
|
||||
|
||||
groupMultiselect<T>(opts: {
|
||||
message: string;
|
||||
options: Record<string, MultiSelectOption<T>[]>;
|
||||
required?: boolean;
|
||||
}): Promise<T[]>;
|
||||
|
||||
spinner(): ProgressHandle;
|
||||
|
||||
separator(): void;
|
||||
}
|
||||
83
packages/mosaic/src/runtime/detector.ts
Normal file
83
packages/mosaic/src/runtime/detector.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
import type { RuntimeName } from '../types.js';
|
||||
|
||||
export interface RuntimeInfo {
|
||||
name: RuntimeName;
|
||||
label: string;
|
||||
installed: boolean;
|
||||
path?: string;
|
||||
version?: string;
|
||||
installHint: string;
|
||||
}
|
||||
|
||||
const RUNTIME_DEFS: Record<
|
||||
RuntimeName,
|
||||
{ label: string; command: string; versionFlag: string; installHint: string }
|
||||
> = {
|
||||
claude: {
|
||||
label: 'Claude Code',
|
||||
command: 'claude',
|
||||
versionFlag: '--version',
|
||||
installHint: 'npm install -g @anthropic-ai/claude-code',
|
||||
},
|
||||
codex: {
|
||||
label: 'Codex',
|
||||
command: 'codex',
|
||||
versionFlag: '--version',
|
||||
installHint: 'npm install -g @openai/codex',
|
||||
},
|
||||
opencode: {
|
||||
label: 'OpenCode',
|
||||
command: 'opencode',
|
||||
versionFlag: 'version',
|
||||
installHint: 'See https://opencode.ai for install instructions',
|
||||
},
|
||||
};
|
||||
|
||||
export function detectRuntime(name: RuntimeName): RuntimeInfo {
|
||||
const def = RUNTIME_DEFS[name];
|
||||
const isWindows = platform() === 'win32';
|
||||
const whichCmd = isWindows
|
||||
? `where ${def.command} 2>nul`
|
||||
: `which ${def.command} 2>/dev/null`;
|
||||
|
||||
try {
|
||||
const path = execSync(whichCmd, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
})
|
||||
.trim()
|
||||
.split('\n')[0];
|
||||
|
||||
let version: string | undefined;
|
||||
try {
|
||||
version = execSync(`${def.command} ${def.versionFlag} 2>/dev/null`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim();
|
||||
} catch {
|
||||
// Version detection is optional
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
label: def.label,
|
||||
installed: true,
|
||||
path,
|
||||
version,
|
||||
installHint: def.installHint,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name,
|
||||
label: def.label,
|
||||
installed: false,
|
||||
installHint: def.installHint,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getInstallInstructions(name: RuntimeName): string {
|
||||
return RUNTIME_DEFS[name].installHint;
|
||||
}
|
||||
12
packages/mosaic/src/runtime/installer.ts
Normal file
12
packages/mosaic/src/runtime/installer.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { RuntimeName } from '../types.js';
|
||||
import { getInstallInstructions } from './detector.js';
|
||||
|
||||
export function formatInstallInstructions(name: RuntimeName): string {
|
||||
const hint = getInstallInstructions(name);
|
||||
const labels: Record<RuntimeName, string> = {
|
||||
claude: 'Claude Code',
|
||||
codex: 'Codex',
|
||||
opencode: 'OpenCode',
|
||||
};
|
||||
return `To install ${labels[name]}:\n ${hint}`;
|
||||
}
|
||||
112
packages/mosaic/src/runtime/mcp-config.ts
Normal file
112
packages/mosaic/src/runtime/mcp-config.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { RuntimeName } from '../types.js';
|
||||
|
||||
const MCP_ENTRY = {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
||||
};
|
||||
|
||||
export function configureMcpForRuntime(runtime: RuntimeName): void {
|
||||
switch (runtime) {
|
||||
case 'claude':
|
||||
return configureClaudeMcp();
|
||||
case 'codex':
|
||||
return configureCodexMcp();
|
||||
case 'opencode':
|
||||
return configureOpenCodeMcp();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDir(filePath: string): void {
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
}
|
||||
|
||||
function configureClaudeMcp(): void {
|
||||
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
||||
ensureDir(settingsPath);
|
||||
|
||||
let data: Record<string, unknown> = {};
|
||||
if (existsSync(settingsPath)) {
|
||||
try {
|
||||
data = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
} catch {
|
||||
// Start fresh if corrupt
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!data.mcpServers ||
|
||||
typeof data.mcpServers !== 'object' ||
|
||||
Array.isArray(data.mcpServers)
|
||||
) {
|
||||
data.mcpServers = {};
|
||||
}
|
||||
(data.mcpServers as Record<string, unknown>)['sequential-thinking'] =
|
||||
MCP_ENTRY;
|
||||
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(data, null, 2) + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
function configureCodexMcp(): void {
|
||||
const configPath = join(homedir(), '.codex', 'config.toml');
|
||||
ensureDir(configPath);
|
||||
|
||||
let content = '';
|
||||
if (existsSync(configPath)) {
|
||||
content = readFileSync(configPath, 'utf-8');
|
||||
// Remove existing sequential-thinking section
|
||||
content = content
|
||||
.replace(
|
||||
/\[mcp_servers\.(sequential-thinking|sequential_thinking)\][\s\S]*?(?=\n\[|$)/g,
|
||||
'',
|
||||
)
|
||||
.trim();
|
||||
}
|
||||
|
||||
content +=
|
||||
'\n\n[mcp_servers.sequential-thinking]\n' +
|
||||
'command = "npx"\n' +
|
||||
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]\n';
|
||||
|
||||
writeFileSync(configPath, content, 'utf-8');
|
||||
}
|
||||
|
||||
function configureOpenCodeMcp(): void {
|
||||
const configPath = join(
|
||||
homedir(),
|
||||
'.config',
|
||||
'opencode',
|
||||
'config.json',
|
||||
);
|
||||
ensureDir(configPath);
|
||||
|
||||
let data: Record<string, unknown> = {};
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
||||
} catch {
|
||||
// Start fresh
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.mcp || typeof data.mcp !== 'object' || Array.isArray(data.mcp)) {
|
||||
data.mcp = {};
|
||||
}
|
||||
(data.mcp as Record<string, unknown>)['sequential-thinking'] = {
|
||||
type: 'local',
|
||||
command: ['npx', '-y', '@modelcontextprotocol/server-sequential-thinking'],
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify(data, null, 2) + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
99
packages/mosaic/src/skills/catalog.ts
Normal file
99
packages/mosaic/src/skills/catalog.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { RECOMMENDED_SKILLS } from '../constants.js';
|
||||
|
||||
export interface SkillEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
version?: string;
|
||||
recommended: boolean;
|
||||
source: 'canonical' | 'local';
|
||||
}
|
||||
|
||||
export function loadSkillsCatalog(mosaicHome: string): SkillEntry[] {
|
||||
const skills: SkillEntry[] = [];
|
||||
|
||||
// Load canonical skills
|
||||
const canonicalDir = join(mosaicHome, 'skills');
|
||||
if (existsSync(canonicalDir)) {
|
||||
skills.push(...loadSkillsFromDir(canonicalDir, 'canonical'));
|
||||
}
|
||||
|
||||
// Fallback to source repo
|
||||
const sourceDir = join(mosaicHome, 'sources', 'agent-skills', 'skills');
|
||||
if (skills.length === 0 && existsSync(sourceDir)) {
|
||||
skills.push(...loadSkillsFromDir(sourceDir, 'canonical'));
|
||||
}
|
||||
|
||||
// Load local skills
|
||||
const localDir = join(mosaicHome, 'skills-local');
|
||||
if (existsSync(localDir)) {
|
||||
skills.push(...loadSkillsFromDir(localDir, 'local'));
|
||||
}
|
||||
|
||||
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function loadSkillsFromDir(
|
||||
dir: string,
|
||||
source: 'canonical' | 'local',
|
||||
): SkillEntry[] {
|
||||
const entries: SkillEntry[] = [];
|
||||
|
||||
let dirEntries;
|
||||
try {
|
||||
dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return entries;
|
||||
}
|
||||
|
||||
for (const entry of dirEntries) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
||||
|
||||
const skillMdPath = join(dir, entry.name, 'SKILL.md');
|
||||
if (!existsSync(skillMdPath)) continue;
|
||||
|
||||
try {
|
||||
const content = readFileSync(skillMdPath, 'utf-8');
|
||||
const frontmatter = parseFrontmatter(content);
|
||||
|
||||
entries.push({
|
||||
name: (frontmatter.name as string) ?? entry.name,
|
||||
description: (frontmatter.description as string) ?? '',
|
||||
version: frontmatter.version as string | undefined,
|
||||
recommended: RECOMMENDED_SKILLS.has(entry.name),
|
||||
source,
|
||||
});
|
||||
} catch {
|
||||
// Skip malformed skills
|
||||
entries.push({
|
||||
name: entry.name,
|
||||
description: '',
|
||||
recommended: RECOMMENDED_SKILLS.has(entry.name),
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseFrontmatter(content: string): Record<string, unknown> {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!match) return {};
|
||||
|
||||
try {
|
||||
return (parseYaml(match[1]) as Record<string, unknown>) ?? {};
|
||||
} catch {
|
||||
// Fallback: simple key-value parsing
|
||||
const result: Record<string, string> = {};
|
||||
for (const line of match[1].split('\n')) {
|
||||
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)/);
|
||||
if (kv) {
|
||||
result[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
86
packages/mosaic/src/skills/categories.ts
Normal file
86
packages/mosaic/src/skills/categories.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Skill category definitions and mapping.
|
||||
* Skills are assigned to categories by name, with keyword fallback.
|
||||
*/
|
||||
|
||||
export const SKILL_CATEGORIES: Record<string, string[]> = {
|
||||
'Frontend & UI': [
|
||||
'ai-sdk', 'algorithmic-art', 'antfu', 'canvas-design', 'frontend-design',
|
||||
'next-best-practices', 'nuxt', 'pinia', 'shadcn-ui', 'slidev',
|
||||
'tailwind-design-system', 'theme-factory', 'ui-animation', 'unocss',
|
||||
'vercel-composition-patterns', 'vercel-react-best-practices',
|
||||
'vercel-react-native-skills', 'vue', 'vue-best-practices',
|
||||
'vue-router-best-practices', 'vueuse-functions', 'web-artifacts-builder',
|
||||
'web-design-guidelines', 'vite', 'vitepress',
|
||||
],
|
||||
'Backend & Infrastructure': [
|
||||
'architecture-patterns', 'fastapi', 'mcp-builder', 'nestjs-best-practices',
|
||||
'python-performance-optimization', 'tsdown', 'turborepo', 'pnpm',
|
||||
'dispatching-parallel-agents', 'subagent-driven-development', 'create-agent',
|
||||
'proactive-agent', 'using-superpowers', 'kickstart', 'executing-plans',
|
||||
],
|
||||
'Testing & Quality': [
|
||||
'code-review-excellence', 'lint', 'pr-reviewer', 'receiving-code-review',
|
||||
'requesting-code-review', 'systematic-debugging', 'test-driven-development',
|
||||
'verification-before-completion', 'vitest', 'vue-testing-best-practices',
|
||||
'webapp-testing',
|
||||
],
|
||||
'Marketing & Growth': [
|
||||
'ab-test-setup', 'analytics-tracking', 'competitor-alternatives',
|
||||
'copy-editing', 'copywriting', 'email-sequence', 'form-cro',
|
||||
'free-tool-strategy', 'launch-strategy', 'marketing-ideas',
|
||||
'marketing-psychology', 'onboarding-cro', 'page-cro', 'paid-ads',
|
||||
'paywall-upgrade-cro', 'popup-cro', 'pricing-strategy',
|
||||
'product-marketing-context', 'programmatic-seo', 'referral-program',
|
||||
'schema-markup', 'seo-audit', 'signup-flow-cro', 'social-content',
|
||||
],
|
||||
'Product & Strategy': [
|
||||
'brainstorming', 'brand-guidelines', 'content-strategy',
|
||||
'writing-plans', 'skill-creator', 'writing-skills', 'prd',
|
||||
],
|
||||
'Developer Practices': [
|
||||
'finishing-a-development-branch', 'using-git-worktrees',
|
||||
],
|
||||
'Auth & Security': [
|
||||
'better-auth-best-practices', 'create-auth-skill',
|
||||
'email-and-password-best-practices', 'organization-best-practices',
|
||||
'two-factor-authentication-best-practices',
|
||||
],
|
||||
'Content & Documentation': [
|
||||
'doc-coauthoring', 'docx', 'internal-comms', 'pdf', 'pptx',
|
||||
'slack-gif-creator', 'xlsx',
|
||||
],
|
||||
};
|
||||
|
||||
// Reverse lookup: skill name -> category
|
||||
const SKILL_TO_CATEGORY = new Map<string, string>();
|
||||
for (const [category, skills] of Object.entries(SKILL_CATEGORIES)) {
|
||||
for (const skill of skills) {
|
||||
SKILL_TO_CATEGORY.set(skill, category);
|
||||
}
|
||||
}
|
||||
|
||||
export function categorizeSkill(name: string, description: string): string {
|
||||
const mapped = SKILL_TO_CATEGORY.get(name);
|
||||
if (mapped) return mapped;
|
||||
return inferCategoryFromDescription(description);
|
||||
}
|
||||
|
||||
function inferCategoryFromDescription(desc: string): string {
|
||||
const lower = desc.toLowerCase();
|
||||
if (/\b(react|vue|css|frontend|ui|component|tailwind|design)\b/.test(lower))
|
||||
return 'Frontend & UI';
|
||||
if (/\b(api|backend|server|docker|infra|deploy)\b/.test(lower))
|
||||
return 'Backend & Infrastructure';
|
||||
if (/\b(test|lint|review|debug|quality)\b/.test(lower))
|
||||
return 'Testing & Quality';
|
||||
if (/\b(marketing|seo|copy|ads|cro|conversion|email)\b/.test(lower))
|
||||
return 'Marketing & Growth';
|
||||
if (/\b(auth|security|2fa|password|credential)\b/.test(lower))
|
||||
return 'Auth & Security';
|
||||
if (/\b(doc|pdf|word|sheet|writing|comms)\b/.test(lower))
|
||||
return 'Content & Documentation';
|
||||
if (/\b(product|strategy|brainstorm|plan|prd)\b/.test(lower))
|
||||
return 'Product & Strategy';
|
||||
return 'Developer Practices';
|
||||
}
|
||||
95
packages/mosaic/src/stages/detect-install.ts
Normal file
95
packages/mosaic/src/stages/detect-install.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { ConfigService } from '../config/config-service.js';
|
||||
import type { WizardState, InstallAction } from '../types.js';
|
||||
|
||||
function detectExistingInstall(mosaicHome: string): boolean {
|
||||
if (!existsSync(mosaicHome)) return false;
|
||||
return (
|
||||
existsSync(join(mosaicHome, 'bin/mosaic')) ||
|
||||
existsSync(join(mosaicHome, 'AGENTS.md')) ||
|
||||
existsSync(join(mosaicHome, 'SOUL.md'))
|
||||
);
|
||||
}
|
||||
|
||||
function detectExistingIdentity(mosaicHome: string): {
|
||||
hasSoul: boolean;
|
||||
hasUser: boolean;
|
||||
hasTools: boolean;
|
||||
agentName?: string;
|
||||
} {
|
||||
const soulPath = join(mosaicHome, 'SOUL.md');
|
||||
const hasSoul = existsSync(soulPath);
|
||||
let agentName: string | undefined;
|
||||
|
||||
if (hasSoul) {
|
||||
try {
|
||||
const content = readFileSync(soulPath, 'utf-8');
|
||||
const match = content.match(/You are \*\*(.+?)\*\*/);
|
||||
agentName = match?.[1];
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasSoul,
|
||||
hasUser: existsSync(join(mosaicHome, 'USER.md')),
|
||||
hasTools: existsSync(join(mosaicHome, 'TOOLS.md')),
|
||||
agentName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function detectInstallStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
config: ConfigService,
|
||||
): Promise<void> {
|
||||
const existing = detectExistingInstall(state.mosaicHome);
|
||||
if (!existing) {
|
||||
state.installAction = 'fresh';
|
||||
return;
|
||||
}
|
||||
|
||||
const identity = detectExistingIdentity(state.mosaicHome);
|
||||
const identitySummary = identity.agentName
|
||||
? `Agent: ${identity.agentName}`
|
||||
: 'No identity configured';
|
||||
|
||||
p.note(
|
||||
`Found existing Mosaic installation at:\n${state.mosaicHome}\n\n` +
|
||||
`${identitySummary}\n` +
|
||||
`SOUL.md: ${identity.hasSoul ? 'yes' : 'no'}\n` +
|
||||
`USER.md: ${identity.hasUser ? 'yes' : 'no'}\n` +
|
||||
`TOOLS.md: ${identity.hasTools ? 'yes' : 'no'}`,
|
||||
'Existing Installation Detected',
|
||||
);
|
||||
|
||||
state.installAction = await p.select<InstallAction>({
|
||||
message: 'What would you like to do?',
|
||||
options: [
|
||||
{
|
||||
value: 'keep',
|
||||
label: 'Keep identity, update framework',
|
||||
hint: 'Preserves SOUL.md, USER.md, TOOLS.md, memory/',
|
||||
},
|
||||
{
|
||||
value: 'reconfigure',
|
||||
label: 'Reconfigure identity',
|
||||
hint: 'Re-run identity setup, update framework',
|
||||
},
|
||||
{
|
||||
value: 'reset',
|
||||
label: 'Fresh install',
|
||||
hint: 'Replace everything',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (state.installAction === 'keep') {
|
||||
state.soul = await config.readSoul();
|
||||
state.user = await config.readUser();
|
||||
state.tools = await config.readTools();
|
||||
}
|
||||
}
|
||||
177
packages/mosaic/src/stages/finalize.ts
Normal file
177
packages/mosaic/src/stages/finalize.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { platform } from 'node:os';
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { ConfigService } from '../config/config-service.js';
|
||||
import type { WizardState } from '../types.js';
|
||||
import { getShellProfilePath } from '../platform/detect.js';
|
||||
|
||||
function linkRuntimeAssets(mosaicHome: string): void {
|
||||
const script = join(mosaicHome, 'bin', 'mosaic-link-runtime-assets');
|
||||
if (existsSync(script)) {
|
||||
try {
|
||||
spawnSync('bash', [script], { timeout: 30000, stdio: 'pipe' });
|
||||
} catch {
|
||||
// Non-fatal: wizard continues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncSkills(mosaicHome: string): void {
|
||||
const script = join(mosaicHome, 'bin', 'mosaic-sync-skills');
|
||||
if (existsSync(script)) {
|
||||
try {
|
||||
spawnSync('bash', [script], { timeout: 60000, stdio: 'pipe' });
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DoctorResult {
|
||||
warnings: number;
|
||||
output: string;
|
||||
}
|
||||
|
||||
function runDoctor(mosaicHome: string): DoctorResult {
|
||||
const script = join(mosaicHome, 'bin', 'mosaic-doctor');
|
||||
if (!existsSync(script)) {
|
||||
return { warnings: 0, output: 'mosaic-doctor not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = spawnSync('bash', [script], {
|
||||
timeout: 30000,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const output = result.stdout ?? '';
|
||||
const warnings = (output.match(/WARN/g) ?? []).length;
|
||||
return { warnings, output };
|
||||
} catch {
|
||||
return { warnings: 1, output: 'Doctor check failed' };
|
||||
}
|
||||
}
|
||||
|
||||
type PathAction = 'already' | 'added' | 'skipped';
|
||||
|
||||
function setupPath(
|
||||
mosaicHome: string,
|
||||
p: WizardPrompter,
|
||||
): PathAction {
|
||||
const binDir = join(mosaicHome, 'bin');
|
||||
const currentPath = process.env.PATH ?? '';
|
||||
|
||||
if (currentPath.includes(binDir)) {
|
||||
return 'already';
|
||||
}
|
||||
|
||||
const profilePath = getShellProfilePath();
|
||||
if (!profilePath) return 'skipped';
|
||||
|
||||
const isWindows = platform() === 'win32';
|
||||
const exportLine = isWindows
|
||||
? `\n# Mosaic\n$env:Path = "${binDir};$env:Path"\n`
|
||||
: `\n# Mosaic\nexport PATH="${binDir}:$PATH"\n`;
|
||||
|
||||
// Check if already in profile
|
||||
if (existsSync(profilePath)) {
|
||||
const content = readFileSync(profilePath, 'utf-8');
|
||||
if (content.includes(binDir)) {
|
||||
return 'already';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
appendFileSync(profilePath, exportLine, 'utf-8');
|
||||
return 'added';
|
||||
} catch {
|
||||
return 'skipped';
|
||||
}
|
||||
}
|
||||
|
||||
export async function finalizeStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
config: ConfigService,
|
||||
): Promise<void> {
|
||||
p.separator();
|
||||
|
||||
const spin = p.spinner();
|
||||
|
||||
// 1. Sync framework files (before config writes so identity files aren't overwritten)
|
||||
spin.update('Syncing framework files...');
|
||||
await config.syncFramework(state.installAction);
|
||||
|
||||
// 2. Write config files (after sync so they aren't overwritten by source templates)
|
||||
if (state.installAction !== 'keep') {
|
||||
spin.update('Writing configuration files...');
|
||||
await config.writeSoul(state.soul);
|
||||
await config.writeUser(state.user);
|
||||
await config.writeTools(state.tools);
|
||||
}
|
||||
|
||||
// 3. Link runtime assets
|
||||
spin.update('Linking runtime assets...');
|
||||
linkRuntimeAssets(state.mosaicHome);
|
||||
|
||||
// 4. Sync skills
|
||||
if (state.selectedSkills.length > 0) {
|
||||
spin.update('Syncing skills...');
|
||||
syncSkills(state.mosaicHome);
|
||||
}
|
||||
|
||||
// 5. Run doctor
|
||||
spin.update('Running health audit...');
|
||||
const doctorResult = runDoctor(state.mosaicHome);
|
||||
|
||||
spin.stop('Installation complete');
|
||||
|
||||
// 6. PATH setup
|
||||
const pathAction = setupPath(state.mosaicHome, p);
|
||||
|
||||
// 7. Summary
|
||||
const summary: string[] = [
|
||||
`Agent: ${state.soul.agentName ?? 'Assistant'}`,
|
||||
`Style: ${state.soul.communicationStyle ?? 'direct'}`,
|
||||
`Runtimes: ${state.runtimes.detected.join(', ') || 'none detected'}`,
|
||||
`Skills: ${state.selectedSkills.length} selected`,
|
||||
`Config: ${state.mosaicHome}`,
|
||||
];
|
||||
|
||||
if (doctorResult.warnings > 0) {
|
||||
summary.push(
|
||||
`Health: ${doctorResult.warnings} warning(s) — run 'mosaic doctor' for details`,
|
||||
);
|
||||
} else {
|
||||
summary.push('Health: all checks passed');
|
||||
}
|
||||
|
||||
p.note(summary.join('\n'), 'Installation Summary');
|
||||
|
||||
// 8. Next steps
|
||||
const nextSteps: string[] = [];
|
||||
if (pathAction === 'added') {
|
||||
const profilePath = getShellProfilePath();
|
||||
nextSteps.push(
|
||||
`Reload shell: source ${profilePath ?? '~/.profile'}`,
|
||||
);
|
||||
}
|
||||
if (state.runtimes.detected.length === 0) {
|
||||
nextSteps.push(
|
||||
'Install at least one runtime (claude, codex, or opencode)',
|
||||
);
|
||||
}
|
||||
nextSteps.push("Launch with 'mosaic claude' (or codex/opencode)");
|
||||
nextSteps.push(
|
||||
'Edit identity files directly in ~/.config/mosaic/ for fine-tuning',
|
||||
);
|
||||
|
||||
p.note(
|
||||
nextSteps.map((s, i) => `${i + 1}. ${s}`).join('\n'),
|
||||
'Next Steps',
|
||||
);
|
||||
|
||||
p.outro('Mosaic is ready.');
|
||||
}
|
||||
23
packages/mosaic/src/stages/mode-select.ts
Normal file
23
packages/mosaic/src/stages/mode-select.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState, WizardMode } from '../types.js';
|
||||
|
||||
export async function modeSelectStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
): Promise<void> {
|
||||
state.mode = await p.select<WizardMode>({
|
||||
message: 'Installation mode',
|
||||
options: [
|
||||
{
|
||||
value: 'quick',
|
||||
label: 'Quick Start',
|
||||
hint: 'Sensible defaults, minimal questions (~2 min)',
|
||||
},
|
||||
{
|
||||
value: 'advanced',
|
||||
label: 'Advanced',
|
||||
hint: 'Full customization of identity, runtimes, and skills',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
70
packages/mosaic/src/stages/runtime-setup.ts
Normal file
70
packages/mosaic/src/stages/runtime-setup.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState, RuntimeName } from '../types.js';
|
||||
import { detectRuntime, type RuntimeInfo } from '../runtime/detector.js';
|
||||
import { formatInstallInstructions } from '../runtime/installer.js';
|
||||
import { configureMcpForRuntime } from '../runtime/mcp-config.js';
|
||||
|
||||
const RUNTIME_NAMES: RuntimeName[] = ['claude', 'codex', 'opencode'];
|
||||
|
||||
export async function runtimeSetupStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
): Promise<void> {
|
||||
p.separator();
|
||||
|
||||
const spin = p.spinner();
|
||||
spin.update('Detecting installed runtimes...');
|
||||
|
||||
const runtimes: RuntimeInfo[] = RUNTIME_NAMES.map(detectRuntime);
|
||||
|
||||
spin.stop('Runtime detection complete');
|
||||
|
||||
const detected = runtimes.filter((r) => r.installed);
|
||||
const notDetected = runtimes.filter((r) => !r.installed);
|
||||
|
||||
if (detected.length > 0) {
|
||||
const summary = detected
|
||||
.map(
|
||||
(r) =>
|
||||
` ${r.label}: ${r.version ?? 'installed'} (${r.path})`,
|
||||
)
|
||||
.join('\n');
|
||||
p.note(summary, 'Detected Runtimes');
|
||||
} else {
|
||||
p.warn('No runtimes detected. Install at least one to use Mosaic.');
|
||||
}
|
||||
|
||||
state.runtimes.detected = detected.map((r) => r.name);
|
||||
|
||||
// Offer installation info for missing runtimes in advanced mode
|
||||
if (state.mode === 'advanced' && notDetected.length > 0) {
|
||||
const showInstall = await p.confirm({
|
||||
message: `${notDetected.length} runtime(s) not found. Show install instructions?`,
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
if (showInstall) {
|
||||
for (const rt of notDetected) {
|
||||
p.note(formatInstallInstructions(rt.name), `Install ${rt.label}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure MCP sequential-thinking for detected runtimes
|
||||
if (detected.length > 0) {
|
||||
const spin2 = p.spinner();
|
||||
spin2.update('Configuring sequential-thinking MCP...');
|
||||
try {
|
||||
for (const rt of detected) {
|
||||
configureMcpForRuntime(rt.name);
|
||||
}
|
||||
spin2.stop('MCP sequential-thinking configured');
|
||||
state.runtimes.mcpConfigured = true;
|
||||
} catch (err) {
|
||||
spin2.stop('MCP configuration failed (non-fatal)');
|
||||
p.warn(
|
||||
`MCP setup failed: ${err instanceof Error ? err.message : String(err)}. Run 'mosaic seq fix' later.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
packages/mosaic/src/stages/skills-select.ts
Normal file
84
packages/mosaic/src/stages/skills-select.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState } from '../types.js';
|
||||
import { loadSkillsCatalog } from '../skills/catalog.js';
|
||||
import { SKILL_CATEGORIES, categorizeSkill } from '../skills/categories.js';
|
||||
|
||||
function truncate(str: string, max: number): string {
|
||||
if (str.length <= max) return str;
|
||||
return str.slice(0, max - 1) + '\u2026';
|
||||
}
|
||||
|
||||
export async function skillsSelectStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
): Promise<void> {
|
||||
p.separator();
|
||||
|
||||
const spin = p.spinner();
|
||||
spin.update('Loading skills catalog...');
|
||||
|
||||
const catalog = loadSkillsCatalog(state.mosaicHome);
|
||||
spin.stop(`Found ${catalog.length} available skills`);
|
||||
|
||||
if (catalog.length === 0) {
|
||||
p.warn(
|
||||
"No skills found. Run 'mosaic sync' after installation to fetch skills.",
|
||||
);
|
||||
state.selectedSkills = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mode === 'quick') {
|
||||
const defaults = catalog
|
||||
.filter((s) => s.recommended)
|
||||
.map((s) => s.name);
|
||||
state.selectedSkills = defaults;
|
||||
p.note(
|
||||
`Selected ${defaults.length} recommended skills.\n` +
|
||||
`Run 'mosaic sync' later to browse the full catalog.`,
|
||||
'Skills',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Advanced mode: categorized browsing
|
||||
p.note(
|
||||
'Skills give agents domain expertise for specific tasks.\n' +
|
||||
'Browse by category and select the ones you want.\n' +
|
||||
"You can always change this later with 'mosaic sync'.",
|
||||
'Skills Selection',
|
||||
);
|
||||
|
||||
// Build grouped options
|
||||
const grouped: Record<
|
||||
string,
|
||||
{ value: string; label: string; hint?: string; selected?: boolean }[]
|
||||
> = {};
|
||||
|
||||
// Initialize all categories
|
||||
for (const categoryName of Object.keys(SKILL_CATEGORIES)) {
|
||||
grouped[categoryName] = [];
|
||||
}
|
||||
|
||||
for (const skill of catalog) {
|
||||
const category = categorizeSkill(skill.name, skill.description);
|
||||
if (!grouped[category]) grouped[category] = [];
|
||||
grouped[category].push({
|
||||
value: skill.name,
|
||||
label: skill.name,
|
||||
hint: truncate(skill.description, 60),
|
||||
selected: skill.recommended,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove empty categories
|
||||
for (const key of Object.keys(grouped)) {
|
||||
if (grouped[key].length === 0) delete grouped[key];
|
||||
}
|
||||
|
||||
state.selectedSkills = await p.groupMultiselect({
|
||||
message: 'Select skills (space to toggle)',
|
||||
options: grouped,
|
||||
required: false,
|
||||
});
|
||||
}
|
||||
73
packages/mosaic/src/stages/soul-setup.ts
Normal file
73
packages/mosaic/src/stages/soul-setup.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState, CommunicationStyle } from '../types.js';
|
||||
import { DEFAULTS } from '../constants.js';
|
||||
|
||||
export async function soulSetupStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
): Promise<void> {
|
||||
if (state.installAction === 'keep') return;
|
||||
|
||||
p.separator();
|
||||
p.note(
|
||||
'Your agent identity defines how AI assistants behave,\n' +
|
||||
'their principles, and communication style.\n' +
|
||||
'This creates SOUL.md.',
|
||||
'Agent Identity',
|
||||
);
|
||||
|
||||
if (!state.soul.agentName) {
|
||||
state.soul.agentName = await p.text({
|
||||
message: 'What name should agents use?',
|
||||
placeholder: 'e.g., Jarvis, Assistant, Mosaic',
|
||||
defaultValue: DEFAULTS.agentName,
|
||||
validate: (v) => {
|
||||
if (v.length === 0) return 'Name cannot be empty';
|
||||
if (v.length > 50) return 'Name must be under 50 characters';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (state.mode === 'advanced') {
|
||||
if (!state.soul.roleDescription) {
|
||||
state.soul.roleDescription = await p.text({
|
||||
message: 'Agent role description',
|
||||
placeholder: 'e.g., execution partner and visibility engine',
|
||||
defaultValue: DEFAULTS.roleDescription,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
state.soul.roleDescription ??= DEFAULTS.roleDescription;
|
||||
}
|
||||
|
||||
if (!state.soul.communicationStyle) {
|
||||
state.soul.communicationStyle = await p.select<CommunicationStyle>({
|
||||
message: 'Communication style',
|
||||
options: [
|
||||
{ value: 'direct', label: 'Direct', hint: 'Concise, no fluff, actionable' },
|
||||
{ value: 'friendly', label: 'Friendly', hint: 'Warm but efficient, conversational' },
|
||||
{ value: 'formal', label: 'Formal', hint: 'Professional, structured, thorough' },
|
||||
],
|
||||
initialValue: 'direct',
|
||||
});
|
||||
}
|
||||
|
||||
if (state.mode === 'advanced') {
|
||||
if (!state.soul.accessibility) {
|
||||
state.soul.accessibility = await p.text({
|
||||
message: 'Accessibility preferences',
|
||||
placeholder:
|
||||
"e.g., ADHD-friendly chunking, dyslexia-aware formatting, or 'none'",
|
||||
defaultValue: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
if (!state.soul.customGuardrails) {
|
||||
state.soul.customGuardrails = await p.text({
|
||||
message: 'Custom guardrails (optional)',
|
||||
placeholder: 'e.g., Never auto-commit to main',
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
76
packages/mosaic/src/stages/tools-setup.ts
Normal file
76
packages/mosaic/src/stages/tools-setup.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState, GitProvider } from '../types.js';
|
||||
import { DEFAULTS } from '../constants.js';
|
||||
|
||||
export async function toolsSetupStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
): Promise<void> {
|
||||
if (state.installAction === 'keep') return;
|
||||
|
||||
if (state.mode === 'quick') {
|
||||
state.tools.gitProviders = [];
|
||||
state.tools.credentialsLocation = DEFAULTS.credentialsLocation;
|
||||
state.tools.customToolsSection = DEFAULTS.customToolsSection;
|
||||
return;
|
||||
}
|
||||
|
||||
p.separator();
|
||||
p.note(
|
||||
'Tool configuration tells agents about your git providers,\n' +
|
||||
'credential locations, and custom tools.\n' +
|
||||
'This creates TOOLS.md.',
|
||||
'Tool Reference',
|
||||
);
|
||||
|
||||
const addProviders = await p.confirm({
|
||||
message: 'Configure git providers?',
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
state.tools.gitProviders = [];
|
||||
if (addProviders) {
|
||||
let addMore = true;
|
||||
while (addMore) {
|
||||
const name = await p.text({
|
||||
message: 'Provider name',
|
||||
placeholder: 'e.g., Gitea, GitHub',
|
||||
});
|
||||
const url = await p.text({
|
||||
message: 'Provider URL',
|
||||
placeholder: 'e.g., https://github.com',
|
||||
});
|
||||
const cli = await p.select<string>({
|
||||
message: 'CLI tool',
|
||||
options: [
|
||||
{ value: 'gh', label: 'gh (GitHub CLI)' },
|
||||
{ value: 'tea', label: 'tea (Gitea CLI)' },
|
||||
{ value: 'glab', label: 'glab (GitLab CLI)' },
|
||||
],
|
||||
});
|
||||
const purpose = await p.text({
|
||||
message: 'Purpose',
|
||||
placeholder: 'e.g., Primary code hosting',
|
||||
defaultValue: 'Code hosting',
|
||||
});
|
||||
|
||||
state.tools.gitProviders.push({
|
||||
name,
|
||||
url,
|
||||
cli,
|
||||
purpose,
|
||||
} satisfies GitProvider);
|
||||
|
||||
addMore = await p.confirm({
|
||||
message: 'Add another provider?',
|
||||
initialValue: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
state.tools.credentialsLocation = await p.text({
|
||||
message: 'Credential file path',
|
||||
placeholder: "e.g., ~/.secrets/credentials.env, or 'none'",
|
||||
defaultValue: DEFAULTS.credentialsLocation,
|
||||
});
|
||||
}
|
||||
80
packages/mosaic/src/stages/user-setup.ts
Normal file
80
packages/mosaic/src/stages/user-setup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState } from '../types.js';
|
||||
import { DEFAULTS } from '../constants.js';
|
||||
import { buildCommunicationPrefs } from '../template/builders.js';
|
||||
|
||||
export async function userSetupStage(
|
||||
p: WizardPrompter,
|
||||
state: WizardState,
|
||||
): Promise<void> {
|
||||
if (state.installAction === 'keep') return;
|
||||
|
||||
p.separator();
|
||||
p.note(
|
||||
'Your user profile helps agents understand your context,\n' +
|
||||
'accessibility needs, and communication preferences.\n' +
|
||||
'This creates USER.md.',
|
||||
'User Profile',
|
||||
);
|
||||
|
||||
if (!state.user.userName) {
|
||||
state.user.userName = await p.text({
|
||||
message: 'Your name',
|
||||
placeholder: 'How agents should address you',
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
|
||||
if (!state.user.pronouns) {
|
||||
state.user.pronouns = await p.text({
|
||||
message: 'Your pronouns',
|
||||
placeholder: 'e.g., He/Him, She/Her, They/Them',
|
||||
defaultValue: DEFAULTS.pronouns,
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-detect timezone
|
||||
let detectedTz: string;
|
||||
try {
|
||||
detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
} catch {
|
||||
detectedTz = DEFAULTS.timezone;
|
||||
}
|
||||
|
||||
if (!state.user.timezone) {
|
||||
state.user.timezone = await p.text({
|
||||
message: 'Your timezone',
|
||||
placeholder: `e.g., ${detectedTz}`,
|
||||
defaultValue: detectedTz,
|
||||
});
|
||||
}
|
||||
|
||||
if (state.mode === 'advanced') {
|
||||
state.user.background = await p.text({
|
||||
message: 'Professional background (brief)',
|
||||
placeholder: 'e.g., Full-stack developer, 10 years TypeScript/React',
|
||||
defaultValue: DEFAULTS.background,
|
||||
});
|
||||
|
||||
state.user.accessibilitySection = await p.text({
|
||||
message: 'Neurodivergence / accessibility accommodations',
|
||||
placeholder: 'e.g., ADHD-friendly chunking, or press Enter to skip',
|
||||
defaultValue: DEFAULTS.accessibilitySection,
|
||||
});
|
||||
|
||||
state.user.personalBoundaries = await p.text({
|
||||
message: 'Personal boundaries for agents',
|
||||
placeholder: 'e.g., No unsolicited career advice, or press Enter to skip',
|
||||
defaultValue: DEFAULTS.personalBoundaries,
|
||||
});
|
||||
} else {
|
||||
state.user.background = DEFAULTS.background;
|
||||
state.user.accessibilitySection = DEFAULTS.accessibilitySection;
|
||||
state.user.personalBoundaries = DEFAULTS.personalBoundaries;
|
||||
}
|
||||
|
||||
// Derive communication preferences from SOUL style
|
||||
state.user.communicationPrefs = buildCommunicationPrefs(
|
||||
state.soul.communicationStyle ?? 'direct',
|
||||
);
|
||||
}
|
||||
18
packages/mosaic/src/stages/welcome.ts
Normal file
18
packages/mosaic/src/stages/welcome.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { WizardPrompter } from '../prompter/interface.js';
|
||||
import type { WizardState } from '../types.js';
|
||||
import { VERSION } from '../constants.js';
|
||||
|
||||
export async function welcomeStage(
|
||||
p: WizardPrompter,
|
||||
_state: WizardState,
|
||||
): Promise<void> {
|
||||
p.intro(`Mosaic Installation Wizard v${VERSION}`);
|
||||
p.note(
|
||||
`Mosaic is an agent framework that gives AI coding assistants\n` +
|
||||
`a persistent identity, shared skills, and structured workflows.\n\n` +
|
||||
`It works with Claude Code, Codex, and OpenCode.\n\n` +
|
||||
`All config is stored locally in ~/.config/mosaic/.\n` +
|
||||
`No data is sent anywhere. No accounts required.`,
|
||||
'What is Mosaic?',
|
||||
);
|
||||
}
|
||||
145
packages/mosaic/src/template/builders.ts
Normal file
145
packages/mosaic/src/template/builders.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { CommunicationStyle, SoulConfig, UserConfig, ToolsConfig, GitProvider } from '../types.js';
|
||||
import { DEFAULTS } from '../constants.js';
|
||||
import type { TemplateVars } from './engine.js';
|
||||
|
||||
/**
|
||||
* Build behavioral principles text based on communication style.
|
||||
* Replicates mosaic-init lines 177-204 exactly.
|
||||
*/
|
||||
function buildBehavioralPrinciples(
|
||||
style: CommunicationStyle,
|
||||
accessibility?: string,
|
||||
): string {
|
||||
let principles: string;
|
||||
|
||||
switch (style) {
|
||||
case 'direct':
|
||||
principles = `1. Clarity over performance theater.
|
||||
2. Practical execution over abstract planning.
|
||||
3. Truthfulness over confidence: state uncertainty explicitly.
|
||||
4. Visible state over hidden assumptions.
|
||||
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations.`;
|
||||
break;
|
||||
case 'friendly':
|
||||
principles = `1. Be helpful and approachable while staying efficient.
|
||||
2. Provide context and explain reasoning when helpful.
|
||||
3. Truthfulness over confidence: state uncertainty explicitly.
|
||||
4. Visible state over hidden assumptions.
|
||||
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations.`;
|
||||
break;
|
||||
case 'formal':
|
||||
principles = `1. Maintain professional, structured communication.
|
||||
2. Provide thorough analysis with explicit tradeoffs.
|
||||
3. Truthfulness over confidence: state uncertainty explicitly.
|
||||
4. Document decisions and rationale clearly.
|
||||
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations.`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (accessibility && accessibility !== 'none' && accessibility.length > 0) {
|
||||
principles += `\n6. ${accessibility}.`;
|
||||
}
|
||||
|
||||
return principles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build communication style text based on style choice.
|
||||
* Replicates mosaic-init lines 208-227 exactly.
|
||||
*/
|
||||
function buildCommunicationStyleText(style: CommunicationStyle): string {
|
||||
switch (style) {
|
||||
case 'direct':
|
||||
return `- Be direct, concise, and concrete.
|
||||
- Avoid fluff, hype, and anthropomorphic roleplay.
|
||||
- Do not simulate certainty when facts are missing.
|
||||
- Prefer actionable next steps and explicit tradeoffs.`;
|
||||
case 'friendly':
|
||||
return `- Be warm and conversational while staying focused.
|
||||
- Explain your reasoning when it helps the user.
|
||||
- Do not simulate certainty when facts are missing.
|
||||
- Prefer actionable next steps with clear context.`;
|
||||
case 'formal':
|
||||
return `- Use professional, structured language.
|
||||
- Provide thorough explanations with supporting detail.
|
||||
- Do not simulate certainty when facts are missing.
|
||||
- Present options with explicit tradeoffs and recommendations.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build communication preferences for USER.md based on style.
|
||||
* Replicates mosaic-init lines 299-316 exactly.
|
||||
*/
|
||||
function buildCommunicationPrefs(style: CommunicationStyle): string {
|
||||
switch (style) {
|
||||
case 'direct':
|
||||
return `- Direct and concise
|
||||
- No sycophancy
|
||||
- Executive summaries and tables for overview`;
|
||||
case 'friendly':
|
||||
return `- Warm and conversational
|
||||
- Explain reasoning when helpful
|
||||
- Balance thoroughness with brevity`;
|
||||
case 'formal':
|
||||
return `- Professional and structured
|
||||
- Thorough explanations with supporting detail
|
||||
- Formal tone with explicit recommendations`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build git providers markdown table from provider list.
|
||||
* Replicates mosaic-init lines 362-384.
|
||||
*/
|
||||
function buildGitProvidersTable(providers?: GitProvider[]): string {
|
||||
if (!providers || providers.length === 0) {
|
||||
return DEFAULTS.gitProvidersTable;
|
||||
}
|
||||
|
||||
const rows = providers
|
||||
.map((p) => `| ${p.name} | ${p.url} | \`${p.cli}\` | ${p.purpose} |`)
|
||||
.join('\n');
|
||||
|
||||
return `| Instance | URL | CLI | Purpose |
|
||||
|----------|-----|-----|---------|
|
||||
${rows}`;
|
||||
}
|
||||
|
||||
export function buildSoulTemplateVars(config: SoulConfig): TemplateVars {
|
||||
const style = config.communicationStyle ?? 'direct';
|
||||
const guardrails = config.customGuardrails
|
||||
? `- ${config.customGuardrails}`
|
||||
: '';
|
||||
|
||||
return {
|
||||
AGENT_NAME: config.agentName ?? DEFAULTS.agentName,
|
||||
ROLE_DESCRIPTION: config.roleDescription ?? DEFAULTS.roleDescription,
|
||||
BEHAVIORAL_PRINCIPLES: buildBehavioralPrinciples(style, config.accessibility),
|
||||
COMMUNICATION_STYLE: buildCommunicationStyleText(style),
|
||||
CUSTOM_GUARDRAILS: guardrails,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildUserTemplateVars(config: UserConfig): TemplateVars {
|
||||
return {
|
||||
USER_NAME: config.userName ?? '',
|
||||
PRONOUNS: config.pronouns ?? DEFAULTS.pronouns,
|
||||
TIMEZONE: config.timezone ?? DEFAULTS.timezone,
|
||||
BACKGROUND: config.background ?? DEFAULTS.background,
|
||||
ACCESSIBILITY_SECTION: config.accessibilitySection ?? DEFAULTS.accessibilitySection,
|
||||
COMMUNICATION_PREFS: config.communicationPrefs ?? buildCommunicationPrefs('direct'),
|
||||
PERSONAL_BOUNDARIES: config.personalBoundaries ?? DEFAULTS.personalBoundaries,
|
||||
PROJECTS_TABLE: config.projectsTable ?? DEFAULTS.projectsTable,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildToolsTemplateVars(config: ToolsConfig): TemplateVars {
|
||||
return {
|
||||
GIT_PROVIDERS_TABLE: buildGitProvidersTable(config.gitProviders),
|
||||
CREDENTIALS_LOCATION: config.credentialsLocation ?? DEFAULTS.credentialsLocation,
|
||||
CUSTOM_TOOLS_SECTION: config.customToolsSection ?? DEFAULTS.customToolsSection,
|
||||
};
|
||||
}
|
||||
|
||||
export { buildCommunicationPrefs };
|
||||
26
packages/mosaic/src/template/engine.ts
Normal file
26
packages/mosaic/src/template/engine.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface TemplateVars {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces {{PLACEHOLDER}} tokens with provided values.
|
||||
* Does NOT expand ${ENV_VAR} syntax — those pass through for shell resolution.
|
||||
*/
|
||||
export function renderTemplate(
|
||||
template: string,
|
||||
vars: TemplateVars,
|
||||
options: { strict?: boolean } = {},
|
||||
): string {
|
||||
return template.replace(
|
||||
/\{\{([A-Z_][A-Z0-9_]*)\}\}/g,
|
||||
(match, varName: string) => {
|
||||
if (varName in vars) {
|
||||
return vars[varName];
|
||||
}
|
||||
if (options.strict) {
|
||||
throw new Error(`Template variable not provided: {{${varName}}}`);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
);
|
||||
}
|
||||
53
packages/mosaic/src/types.ts
Normal file
53
packages/mosaic/src/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type WizardMode = 'quick' | 'advanced';
|
||||
export type InstallAction = 'fresh' | 'keep' | 'reconfigure' | 'reset';
|
||||
export type CommunicationStyle = 'direct' | 'friendly' | 'formal';
|
||||
export type RuntimeName = 'claude' | 'codex' | 'opencode';
|
||||
|
||||
export interface SoulConfig {
|
||||
agentName?: string;
|
||||
roleDescription?: string;
|
||||
communicationStyle?: CommunicationStyle;
|
||||
accessibility?: string;
|
||||
customGuardrails?: string;
|
||||
}
|
||||
|
||||
export interface UserConfig {
|
||||
userName?: string;
|
||||
pronouns?: string;
|
||||
timezone?: string;
|
||||
background?: string;
|
||||
accessibilitySection?: string;
|
||||
communicationPrefs?: string;
|
||||
personalBoundaries?: string;
|
||||
projectsTable?: string;
|
||||
}
|
||||
|
||||
export interface GitProvider {
|
||||
name: string;
|
||||
url: string;
|
||||
cli: string;
|
||||
purpose: string;
|
||||
}
|
||||
|
||||
export interface ToolsConfig {
|
||||
gitProviders?: GitProvider[];
|
||||
credentialsLocation?: string;
|
||||
customToolsSection?: string;
|
||||
}
|
||||
|
||||
export interface RuntimeState {
|
||||
detected: RuntimeName[];
|
||||
mcpConfigured: boolean;
|
||||
}
|
||||
|
||||
export interface WizardState {
|
||||
mosaicHome: string;
|
||||
sourceDir: string;
|
||||
mode: WizardMode;
|
||||
installAction: InstallAction;
|
||||
soul: SoulConfig;
|
||||
user: UserConfig;
|
||||
tools: ToolsConfig;
|
||||
runtimes: RuntimeState;
|
||||
selectedSkills: string[];
|
||||
}
|
||||
96
packages/mosaic/src/wizard.ts
Normal file
96
packages/mosaic/src/wizard.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { WizardPrompter } from './prompter/interface.js';
|
||||
import type { ConfigService } from './config/config-service.js';
|
||||
import type { WizardState } from './types.js';
|
||||
import { welcomeStage } from './stages/welcome.js';
|
||||
import { detectInstallStage } from './stages/detect-install.js';
|
||||
import { modeSelectStage } from './stages/mode-select.js';
|
||||
import { soulSetupStage } from './stages/soul-setup.js';
|
||||
import { userSetupStage } from './stages/user-setup.js';
|
||||
import { toolsSetupStage } from './stages/tools-setup.js';
|
||||
import { runtimeSetupStage } from './stages/runtime-setup.js';
|
||||
import { skillsSelectStage } from './stages/skills-select.js';
|
||||
import { finalizeStage } from './stages/finalize.js';
|
||||
|
||||
export interface WizardOptions {
|
||||
mosaicHome: string;
|
||||
sourceDir: string;
|
||||
prompter: WizardPrompter;
|
||||
configService: ConfigService;
|
||||
cliOverrides?: Partial<WizardState>;
|
||||
}
|
||||
|
||||
export async function runWizard(options: WizardOptions): Promise<void> {
|
||||
const { prompter, configService, mosaicHome, sourceDir } = options;
|
||||
|
||||
const state: WizardState = {
|
||||
mosaicHome,
|
||||
sourceDir,
|
||||
mode: 'quick',
|
||||
installAction: 'fresh',
|
||||
soul: {},
|
||||
user: {},
|
||||
tools: {},
|
||||
runtimes: { detected: [], mcpConfigured: false },
|
||||
selectedSkills: [],
|
||||
};
|
||||
|
||||
// Apply CLI overrides (strip undefined values)
|
||||
if (options.cliOverrides) {
|
||||
if (options.cliOverrides.soul) {
|
||||
for (const [k, v] of Object.entries(options.cliOverrides.soul)) {
|
||||
if (v !== undefined) {
|
||||
(state.soul as Record<string, unknown>)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.cliOverrides.user) {
|
||||
for (const [k, v] of Object.entries(options.cliOverrides.user)) {
|
||||
if (v !== undefined) {
|
||||
(state.user as Record<string, unknown>)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.cliOverrides.tools) {
|
||||
for (const [k, v] of Object.entries(options.cliOverrides.tools)) {
|
||||
if (v !== undefined) {
|
||||
(state.tools as Record<string, unknown>)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.cliOverrides.mode) {
|
||||
state.mode = options.cliOverrides.mode;
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 1: Welcome
|
||||
await welcomeStage(prompter, state);
|
||||
|
||||
// Stage 2: Existing Install Detection
|
||||
await detectInstallStage(prompter, state, configService);
|
||||
|
||||
// Stage 3: Quick Start vs Advanced (skip if keeping existing)
|
||||
if (state.installAction === 'fresh' || state.installAction === 'reset') {
|
||||
await modeSelectStage(prompter, state);
|
||||
} else if (state.installAction === 'reconfigure') {
|
||||
state.mode = 'advanced';
|
||||
}
|
||||
|
||||
// Stage 4: SOUL.md
|
||||
await soulSetupStage(prompter, state);
|
||||
|
||||
// Stage 5: USER.md
|
||||
await userSetupStage(prompter, state);
|
||||
|
||||
// Stage 6: TOOLS.md
|
||||
await toolsSetupStage(prompter, state);
|
||||
|
||||
// Stage 7: Runtime Detection & Installation
|
||||
await runtimeSetupStage(prompter, state);
|
||||
|
||||
// Stage 8: Skills Selection
|
||||
await skillsSelectStage(prompter, state);
|
||||
|
||||
// Stage 9: Finalize
|
||||
await finalizeStage(prompter, state, configService);
|
||||
}
|
||||
|
||||
109
packages/mosaic/tests/integration/full-wizard.test.ts
Normal file
109
packages/mosaic/tests/integration/full-wizard.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
rmSync,
|
||||
cpSync,
|
||||
} from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { HeadlessPrompter } from '../../src/prompter/headless-prompter.js';
|
||||
import { createConfigService } from '../../src/config/config-service.js';
|
||||
import { runWizard } from '../../src/wizard.js';
|
||||
|
||||
describe('Full Wizard (headless)', () => {
|
||||
let tmpDir: string;
|
||||
const repoRoot = join(import.meta.dirname, '..', '..');
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-wizard-test-'));
|
||||
// Copy templates to tmp dir
|
||||
const templatesDir = join(repoRoot, 'templates');
|
||||
if (existsSync(templatesDir)) {
|
||||
cpSync(templatesDir, join(tmpDir, 'templates'), { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('quick start produces valid SOUL.md', async () => {
|
||||
const prompter = new HeadlessPrompter({
|
||||
'Installation mode': 'quick',
|
||||
'What name should agents use?': 'TestBot',
|
||||
'Communication style': 'direct',
|
||||
'Your name': 'Tester',
|
||||
'Your pronouns': 'They/Them',
|
||||
'Your timezone': 'UTC',
|
||||
});
|
||||
|
||||
await runWizard({
|
||||
mosaicHome: tmpDir,
|
||||
sourceDir: tmpDir,
|
||||
prompter,
|
||||
configService: createConfigService(tmpDir, tmpDir),
|
||||
});
|
||||
|
||||
const soulPath = join(tmpDir, 'SOUL.md');
|
||||
expect(existsSync(soulPath)).toBe(true);
|
||||
|
||||
const soul = readFileSync(soulPath, 'utf-8');
|
||||
expect(soul).toContain('You are **TestBot**');
|
||||
expect(soul).toContain('Be direct, concise, and concrete');
|
||||
expect(soul).toContain('execution partner and visibility engine');
|
||||
});
|
||||
|
||||
it('quick start produces valid USER.md', async () => {
|
||||
const prompter = new HeadlessPrompter({
|
||||
'Installation mode': 'quick',
|
||||
'What name should agents use?': 'TestBot',
|
||||
'Communication style': 'direct',
|
||||
'Your name': 'Tester',
|
||||
'Your pronouns': 'He/Him',
|
||||
'Your timezone': 'America/Chicago',
|
||||
});
|
||||
|
||||
await runWizard({
|
||||
mosaicHome: tmpDir,
|
||||
sourceDir: tmpDir,
|
||||
prompter,
|
||||
configService: createConfigService(tmpDir, tmpDir),
|
||||
});
|
||||
|
||||
const userPath = join(tmpDir, 'USER.md');
|
||||
expect(existsSync(userPath)).toBe(true);
|
||||
|
||||
const user = readFileSync(userPath, 'utf-8');
|
||||
expect(user).toContain('**Name:** Tester');
|
||||
expect(user).toContain('**Pronouns:** He/Him');
|
||||
expect(user).toContain('**Timezone:** America/Chicago');
|
||||
});
|
||||
|
||||
it('applies CLI overrides', async () => {
|
||||
const prompter = new HeadlessPrompter({
|
||||
'Installation mode': 'quick',
|
||||
'Your name': 'FromPrompt',
|
||||
});
|
||||
|
||||
await runWizard({
|
||||
mosaicHome: tmpDir,
|
||||
sourceDir: tmpDir,
|
||||
prompter,
|
||||
configService: createConfigService(tmpDir, tmpDir),
|
||||
cliOverrides: {
|
||||
soul: {
|
||||
agentName: 'FromCLI',
|
||||
communicationStyle: 'formal',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const soul = readFileSync(join(tmpDir, 'SOUL.md'), 'utf-8');
|
||||
expect(soul).toContain('You are **FromCLI**');
|
||||
expect(soul).toContain('Use professional, structured language');
|
||||
});
|
||||
});
|
||||
71
packages/mosaic/tests/stages/detect-install.test.ts
Normal file
71
packages/mosaic/tests/stages/detect-install.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { HeadlessPrompter } from '../../src/prompter/headless-prompter.js';
|
||||
import { detectInstallStage } from '../../src/stages/detect-install.js';
|
||||
import type { WizardState } from '../../src/types.js';
|
||||
import type { ConfigService } from '../../src/config/config-service.js';
|
||||
|
||||
function createState(mosaicHome: string): WizardState {
|
||||
return {
|
||||
mosaicHome,
|
||||
sourceDir: mosaicHome,
|
||||
mode: 'quick',
|
||||
installAction: 'fresh',
|
||||
soul: {},
|
||||
user: {},
|
||||
tools: {},
|
||||
runtimes: { detected: [], mcpConfigured: false },
|
||||
selectedSkills: [],
|
||||
};
|
||||
}
|
||||
|
||||
const mockConfig: ConfigService = {
|
||||
readSoul: async () => ({ agentName: 'TestAgent' }),
|
||||
readUser: async () => ({ userName: 'TestUser' }),
|
||||
readTools: async () => ({}),
|
||||
writeSoul: async () => {},
|
||||
writeUser: async () => {},
|
||||
writeTools: async () => {},
|
||||
syncFramework: async () => {},
|
||||
};
|
||||
|
||||
describe('detectInstallStage', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('sets fresh for empty directory', async () => {
|
||||
const p = new HeadlessPrompter({});
|
||||
const state = createState(join(tmpDir, 'nonexistent'));
|
||||
await detectInstallStage(p, state, mockConfig);
|
||||
|
||||
expect(state.installAction).toBe('fresh');
|
||||
});
|
||||
|
||||
it('detects existing install and offers choices', async () => {
|
||||
// Create a mock existing install
|
||||
mkdirSync(join(tmpDir, 'bin'), { recursive: true });
|
||||
writeFileSync(join(tmpDir, 'AGENTS.md'), '# Test');
|
||||
writeFileSync(
|
||||
join(tmpDir, 'SOUL.md'),
|
||||
'You are **Jarvis** in this session.',
|
||||
);
|
||||
|
||||
const p = new HeadlessPrompter({
|
||||
'What would you like to do?': 'keep',
|
||||
});
|
||||
const state = createState(tmpDir);
|
||||
await detectInstallStage(p, state, mockConfig);
|
||||
|
||||
expect(state.installAction).toBe('keep');
|
||||
expect(state.soul.agentName).toBe('TestAgent');
|
||||
});
|
||||
});
|
||||
74
packages/mosaic/tests/stages/soul-setup.test.ts
Normal file
74
packages/mosaic/tests/stages/soul-setup.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HeadlessPrompter } from '../../src/prompter/headless-prompter.js';
|
||||
import { soulSetupStage } from '../../src/stages/soul-setup.js';
|
||||
import type { WizardState } from '../../src/types.js';
|
||||
|
||||
function createState(overrides: Partial<WizardState> = {}): WizardState {
|
||||
return {
|
||||
mosaicHome: '/tmp/test-mosaic',
|
||||
sourceDir: '/tmp/test-mosaic',
|
||||
mode: 'quick',
|
||||
installAction: 'fresh',
|
||||
soul: {},
|
||||
user: {},
|
||||
tools: {},
|
||||
runtimes: { detected: [], mcpConfigured: false },
|
||||
selectedSkills: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('soulSetupStage', () => {
|
||||
it('sets agent name and style in quick mode', async () => {
|
||||
const p = new HeadlessPrompter({
|
||||
'What name should agents use?': 'Jarvis',
|
||||
'Communication style': 'friendly',
|
||||
});
|
||||
|
||||
const state = createState({ mode: 'quick' });
|
||||
await soulSetupStage(p, state);
|
||||
|
||||
expect(state.soul.agentName).toBe('Jarvis');
|
||||
expect(state.soul.communicationStyle).toBe('friendly');
|
||||
expect(state.soul.roleDescription).toBe(
|
||||
'execution partner and visibility engine',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses defaults in quick mode with no answers', async () => {
|
||||
const p = new HeadlessPrompter({});
|
||||
const state = createState({ mode: 'quick' });
|
||||
await soulSetupStage(p, state);
|
||||
|
||||
expect(state.soul.agentName).toBe('Assistant');
|
||||
expect(state.soul.communicationStyle).toBe('direct');
|
||||
});
|
||||
|
||||
it('skips when install action is keep', async () => {
|
||||
const p = new HeadlessPrompter({});
|
||||
const state = createState({ installAction: 'keep' });
|
||||
state.soul.agentName = 'Existing';
|
||||
await soulSetupStage(p, state);
|
||||
|
||||
expect(state.soul.agentName).toBe('Existing');
|
||||
});
|
||||
|
||||
it('asks for all fields in advanced mode', async () => {
|
||||
const p = new HeadlessPrompter({
|
||||
'What name should agents use?': 'Atlas',
|
||||
'Agent role description': 'memory keeper',
|
||||
'Communication style': 'formal',
|
||||
'Accessibility preferences': 'ADHD-friendly',
|
||||
'Custom guardrails (optional)': 'Never push to main',
|
||||
});
|
||||
|
||||
const state = createState({ mode: 'advanced' });
|
||||
await soulSetupStage(p, state);
|
||||
|
||||
expect(state.soul.agentName).toBe('Atlas');
|
||||
expect(state.soul.roleDescription).toBe('memory keeper');
|
||||
expect(state.soul.communicationStyle).toBe('formal');
|
||||
expect(state.soul.accessibility).toBe('ADHD-friendly');
|
||||
expect(state.soul.customGuardrails).toBe('Never push to main');
|
||||
});
|
||||
});
|
||||
60
packages/mosaic/tests/stages/user-setup.test.ts
Normal file
60
packages/mosaic/tests/stages/user-setup.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HeadlessPrompter } from '../../src/prompter/headless-prompter.js';
|
||||
import { userSetupStage } from '../../src/stages/user-setup.js';
|
||||
import type { WizardState } from '../../src/types.js';
|
||||
|
||||
function createState(overrides: Partial<WizardState> = {}): WizardState {
|
||||
return {
|
||||
mosaicHome: '/tmp/test-mosaic',
|
||||
sourceDir: '/tmp/test-mosaic',
|
||||
mode: 'quick',
|
||||
installAction: 'fresh',
|
||||
soul: { communicationStyle: 'direct' },
|
||||
user: {},
|
||||
tools: {},
|
||||
runtimes: { detected: [], mcpConfigured: false },
|
||||
selectedSkills: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('userSetupStage', () => {
|
||||
it('collects basic info in quick mode', async () => {
|
||||
const p = new HeadlessPrompter({
|
||||
'Your name': 'Jason',
|
||||
'Your pronouns': 'He/Him',
|
||||
'Your timezone': 'America/Chicago',
|
||||
});
|
||||
|
||||
const state = createState({ mode: 'quick' });
|
||||
await userSetupStage(p, state);
|
||||
|
||||
expect(state.user.userName).toBe('Jason');
|
||||
expect(state.user.pronouns).toBe('He/Him');
|
||||
expect(state.user.timezone).toBe('America/Chicago');
|
||||
expect(state.user.communicationPrefs).toContain('Direct and concise');
|
||||
});
|
||||
|
||||
it('skips when install action is keep', async () => {
|
||||
const p = new HeadlessPrompter({});
|
||||
const state = createState({ installAction: 'keep' });
|
||||
state.user.userName = 'Existing';
|
||||
await userSetupStage(p, state);
|
||||
|
||||
expect(state.user.userName).toBe('Existing');
|
||||
});
|
||||
|
||||
it('derives communication prefs from soul style', async () => {
|
||||
const p = new HeadlessPrompter({
|
||||
'Your name': 'Test',
|
||||
});
|
||||
|
||||
const state = createState({
|
||||
mode: 'quick',
|
||||
soul: { communicationStyle: 'friendly' },
|
||||
});
|
||||
await userSetupStage(p, state);
|
||||
|
||||
expect(state.user.communicationPrefs).toContain('Warm and conversational');
|
||||
});
|
||||
});
|
||||
99
packages/mosaic/tests/template/builders.test.ts
Normal file
99
packages/mosaic/tests/template/builders.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildSoulTemplateVars,
|
||||
buildUserTemplateVars,
|
||||
buildToolsTemplateVars,
|
||||
} from '../../src/template/builders.js';
|
||||
|
||||
describe('buildSoulTemplateVars', () => {
|
||||
it('builds direct style correctly', () => {
|
||||
const vars = buildSoulTemplateVars({
|
||||
agentName: 'Jarvis',
|
||||
communicationStyle: 'direct',
|
||||
});
|
||||
expect(vars.AGENT_NAME).toBe('Jarvis');
|
||||
expect(vars.BEHAVIORAL_PRINCIPLES).toContain('Clarity over performance theater');
|
||||
expect(vars.COMMUNICATION_STYLE).toContain('Be direct, concise, and concrete');
|
||||
});
|
||||
|
||||
it('builds friendly style correctly', () => {
|
||||
const vars = buildSoulTemplateVars({
|
||||
communicationStyle: 'friendly',
|
||||
});
|
||||
expect(vars.BEHAVIORAL_PRINCIPLES).toContain('Be helpful and approachable');
|
||||
expect(vars.COMMUNICATION_STYLE).toContain('Be warm and conversational');
|
||||
});
|
||||
|
||||
it('builds formal style correctly', () => {
|
||||
const vars = buildSoulTemplateVars({
|
||||
communicationStyle: 'formal',
|
||||
});
|
||||
expect(vars.BEHAVIORAL_PRINCIPLES).toContain('Maintain professional, structured');
|
||||
expect(vars.COMMUNICATION_STYLE).toContain('Use professional, structured language');
|
||||
});
|
||||
|
||||
it('appends accessibility to principles', () => {
|
||||
const vars = buildSoulTemplateVars({
|
||||
communicationStyle: 'direct',
|
||||
accessibility: 'ADHD-friendly chunking',
|
||||
});
|
||||
expect(vars.BEHAVIORAL_PRINCIPLES).toContain('6. ADHD-friendly chunking.');
|
||||
});
|
||||
|
||||
it('does not append accessibility when "none"', () => {
|
||||
const vars = buildSoulTemplateVars({
|
||||
communicationStyle: 'direct',
|
||||
accessibility: 'none',
|
||||
});
|
||||
expect(vars.BEHAVIORAL_PRINCIPLES).not.toContain('6.');
|
||||
});
|
||||
|
||||
it('formats custom guardrails', () => {
|
||||
const vars = buildSoulTemplateVars({
|
||||
customGuardrails: 'Never auto-commit',
|
||||
});
|
||||
expect(vars.CUSTOM_GUARDRAILS).toBe('- Never auto-commit');
|
||||
});
|
||||
|
||||
it('uses defaults when config is empty', () => {
|
||||
const vars = buildSoulTemplateVars({});
|
||||
expect(vars.AGENT_NAME).toBe('Assistant');
|
||||
expect(vars.ROLE_DESCRIPTION).toBe('execution partner and visibility engine');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildUserTemplateVars', () => {
|
||||
it('maps all fields', () => {
|
||||
const vars = buildUserTemplateVars({
|
||||
userName: 'Jason',
|
||||
pronouns: 'He/Him',
|
||||
timezone: 'America/Chicago',
|
||||
});
|
||||
expect(vars.USER_NAME).toBe('Jason');
|
||||
expect(vars.PRONOUNS).toBe('He/Him');
|
||||
expect(vars.TIMEZONE).toBe('America/Chicago');
|
||||
});
|
||||
|
||||
it('uses defaults for missing fields', () => {
|
||||
const vars = buildUserTemplateVars({});
|
||||
expect(vars.PRONOUNS).toBe('They/Them');
|
||||
expect(vars.TIMEZONE).toBe('UTC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildToolsTemplateVars', () => {
|
||||
it('builds git providers table', () => {
|
||||
const vars = buildToolsTemplateVars({
|
||||
gitProviders: [
|
||||
{ name: 'GitHub', url: 'https://github.com', cli: 'gh', purpose: 'OSS' },
|
||||
],
|
||||
});
|
||||
expect(vars.GIT_PROVIDERS_TABLE).toContain('| GitHub |');
|
||||
expect(vars.GIT_PROVIDERS_TABLE).toContain('`gh`');
|
||||
});
|
||||
|
||||
it('uses default table when no providers', () => {
|
||||
const vars = buildToolsTemplateVars({});
|
||||
expect(vars.GIT_PROVIDERS_TABLE).toContain('add your git providers here');
|
||||
});
|
||||
});
|
||||
52
packages/mosaic/tests/template/engine.test.ts
Normal file
52
packages/mosaic/tests/template/engine.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderTemplate } from '../../src/template/engine.js';
|
||||
|
||||
describe('renderTemplate', () => {
|
||||
it('replaces all placeholders', () => {
|
||||
const template = 'You are **{{AGENT_NAME}}**, role: {{ROLE_DESCRIPTION}}';
|
||||
const result = renderTemplate(template, {
|
||||
AGENT_NAME: 'Jarvis',
|
||||
ROLE_DESCRIPTION: 'steward',
|
||||
});
|
||||
expect(result).toBe('You are **Jarvis**, role: steward');
|
||||
});
|
||||
|
||||
it('preserves ${ENV_VAR} references', () => {
|
||||
const template = 'Path: ${HOME}/.config, Agent: {{AGENT_NAME}}';
|
||||
const result = renderTemplate(template, { AGENT_NAME: 'Test' });
|
||||
expect(result).toBe('Path: ${HOME}/.config, Agent: Test');
|
||||
});
|
||||
|
||||
it('handles multi-line values', () => {
|
||||
const template = '{{PRINCIPLES}}';
|
||||
const result = renderTemplate(template, {
|
||||
PRINCIPLES: '1. First\n2. Second\n3. Third',
|
||||
});
|
||||
expect(result).toBe('1. First\n2. Second\n3. Third');
|
||||
});
|
||||
|
||||
it('replaces unset vars with empty string by default', () => {
|
||||
const template = 'Before {{MISSING}} After';
|
||||
const result = renderTemplate(template, {});
|
||||
expect(result).toBe('Before After');
|
||||
});
|
||||
|
||||
it('throws in strict mode for missing vars', () => {
|
||||
const template = '{{MISSING}}';
|
||||
expect(() => renderTemplate(template, {}, { strict: true })).toThrow(
|
||||
'Template variable not provided: {{MISSING}}',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles multiple occurrences of same placeholder', () => {
|
||||
const template = '{{NAME}} says hello, {{NAME}}!';
|
||||
const result = renderTemplate(template, { NAME: 'Jarvis' });
|
||||
expect(result).toBe('Jarvis says hello, Jarvis!');
|
||||
});
|
||||
|
||||
it('preserves non-placeholder curly braces', () => {
|
||||
const template = 'const x = { foo: {{VALUE}} }';
|
||||
const result = renderTemplate(template, { VALUE: '"bar"' });
|
||||
expect(result).toBe('const x = { foo: "bar" }');
|
||||
});
|
||||
});
|
||||
8
packages/mosaic/tsconfig.json
Normal file
8
packages/mosaic/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
601
pnpm-lock.yaml
generated
601
pnpm-lock.yaml
generated
@@ -21,6 +21,40 @@ importers:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
|
||||
packages/mosaic:
|
||||
dependencies:
|
||||
'@clack/prompts':
|
||||
specifier: ^0.9
|
||||
version: 0.9.1
|
||||
commander:
|
||||
specifier: ^13
|
||||
version: 13.1.0
|
||||
picocolors:
|
||||
specifier: ^1.1
|
||||
version: 1.1.1
|
||||
yaml:
|
||||
specifier: ^2.7
|
||||
version: 2.8.2
|
||||
zod:
|
||||
specifier: ^3.24
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22
|
||||
version: 22.19.15
|
||||
tsdown:
|
||||
specifier: ^0.12
|
||||
version: 0.12.9(typescript@5.9.3)
|
||||
tsx:
|
||||
specifier: ^4
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
|
||||
packages/queue:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
@@ -41,7 +75,7 @@ importers:
|
||||
devDependencies:
|
||||
vitest:
|
||||
specifier: ^3
|
||||
version: 3.2.4(@types/node@22.19.15)
|
||||
version: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/types:
|
||||
devDependencies:
|
||||
@@ -67,10 +101,31 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/generator@7.29.1':
|
||||
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.28.6':
|
||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@changesets/apply-release-plan@7.1.0':
|
||||
resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==}
|
||||
|
||||
@@ -126,6 +181,21 @@ packages:
|
||||
'@changesets/write@0.4.0':
|
||||
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
|
||||
|
||||
'@clack/core@0.4.1':
|
||||
resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==}
|
||||
|
||||
'@clack/prompts@0.9.1':
|
||||
resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==}
|
||||
|
||||
'@emnapi/core@1.8.1':
|
||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||
|
||||
'@emnapi/runtime@1.8.1':
|
||||
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -438,9 +508,19 @@ packages:
|
||||
'@ioredis/commands@1.5.1':
|
||||
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
|
||||
|
||||
@@ -457,6 +537,9 @@ packages:
|
||||
'@cfworker/json-schema':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.1':
|
||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -469,6 +552,104 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@oxc-project/types@0.115.0':
|
||||
resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==}
|
||||
|
||||
'@quansync/fs@1.0.0':
|
||||
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-/uadfNUaMLFFBGvcIOiq8NnlhvTZTjOyybJaJnhGxD0n9k5vZRJfTaitH5GHnbwmc6T2PC+ZpS1FQH+vXyS/UA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-zokYr1KgRn0hRA89dmgtPj/BmKp9DxgrfAJvOEFfXa8nfYWW2nmgiYIBGpSIAJrEg7Qc/Qznovy6xYwmKh0M8g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-eZFjbmrapCBVgMmuLALH3pmQQQStHFuRhsFceJHk6KISW8CkI2e9OPLp9V4qXksrySQcD8XM8fpvGLs5l5C7LQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-xjMrh8Dmu2DNwdY6DZsrF6YPGeesc3PaTlkh8v9cqmkSCNeTxnhX3ErhVnuv1j3n8t2IuuhQIwM9eZDINNEt5Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-mOvftrHiXg4/xFdxJY3T9Wl1/zDAOSlMN8z9an2bXsCwuvv3RdyhYbSMZDuDO52S04w9z7+cBd90lvQSPTAQtw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-TuUkeuEEPRyXMBbJ86NRhAiPNezxHW8merl3Om2HASA9Pl1rI+VZcTtsVQ6v/P0MDIFpSl0k0+tUUze9HIXyEw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-G43ZElEvaby+YSOgrXfBgpeQv42LdS0ivFFYQufk2tBDWeBfzE/+ob5DmO8Izbyn4Y8k6GgLF11jFDYNnmU/3w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-Y48ShVxGE2zUTt0A0PR3grCLNxW4DWtAfe5lxf6L3uYEQujwo/LGuRogMsAtOJeYLCPTJo2i714LOdnK34cHpw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-KU5DUYvX3qI8/TX6D3RA4awXi4Ge/1+M6Jqv7kRiUndpqoVGgD765xhV3Q6QvtABnYjLJenrWDl3S1B5U56ixA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-1THb6FdBkAEL12zvUue2bmK4W1+P+tz8Pgu5uEzq+xrtYa3iBzmmKNlyfUzCFNCqsPd8WJEQrYdLcw4iMW4AVw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-12o73atFNWDgYnLyA52QEUn9AH8pHIe12W28cmqjyHt4bIEYRzMICvYVCPa2IQm6DJBvCBrEhD9K+ct4wr2hwg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-+uUgGwvuUCXl894MTsmTS2J0BnCZccFsmzV7y1jFxW5pTSxkuwL5agyPuDvDOztPeS6RrdqWkn7sT0jRd0ECkg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-53p2L/NSy21UiFOqUGlC11kJDZS2Nx2GJRz1QvbkXovypA3cOHbsyZHLkV72JsLSbiEQe+kg4tndUhSiC31UEA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-K6svNRljO6QrL6VTKxwh4yThhlR9DT/tK0XpaFQMnJwwQKng+NYcVEtUkAM0WsoiZHw+Hnh3DGnn3taf/pNYGg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-3ZJBT47VWLKVKIyvHhUSUgVwHzzZW761YAIkM3tOT+8ZTjFVp0acCM0Y2Z2j3jCl+XYi2d9y2uEWQ8H0PvvpPw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
|
||||
cpu: [arm]
|
||||
@@ -594,6 +775,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
@@ -690,6 +874,10 @@ packages:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansis@4.2.0:
|
||||
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
@@ -704,10 +892,17 @@ packages:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-kit@2.2.0:
|
||||
resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
birpc@2.9.0:
|
||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||
|
||||
body-parser@2.2.2:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -743,10 +938,18 @@ packages:
|
||||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
commander@13.1.0:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@14.0.3:
|
||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -788,6 +991,9 @@ packages:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -800,10 +1006,23 @@ packages:
|
||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
diff@8.0.3:
|
||||
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dts-resolver@2.1.3:
|
||||
resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
oxc-resolver: '>=11.0.0'
|
||||
peerDependenciesMeta:
|
||||
oxc-resolver:
|
||||
optional: true
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -811,6 +1030,10 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
empathic@2.0.0:
|
||||
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
encodeurl@2.0.0:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -950,6 +1173,9 @@ packages:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -977,6 +1203,9 @@ packages:
|
||||
resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1034,6 +1263,10 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jiti@2.6.1:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jose@6.2.0:
|
||||
resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==}
|
||||
|
||||
@@ -1048,6 +1281,11 @@ packages:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
@@ -1234,6 +1472,9 @@ packages:
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
quansync@1.0.0:
|
||||
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -1249,6 +1490,10 @@ packages:
|
||||
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1265,10 +1510,34 @@ packages:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
reusify@1.1.0:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rolldown-plugin-dts@0.13.14:
|
||||
resolution: {integrity: sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==}
|
||||
engines: {node: '>=20.18.0'}
|
||||
peerDependencies:
|
||||
'@typescript/native-preview': '>=7.0.0-dev.20250601.1'
|
||||
rolldown: ^1.0.0-beta.9
|
||||
typescript: ^5.0.0
|
||||
vue-tsc: ^2.2.0 || ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
'@typescript/native-preview':
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
vue-tsc:
|
||||
optional: true
|
||||
|
||||
rolldown@1.0.0-rc.7:
|
||||
resolution: {integrity: sha512-5X0zEeQFzDpB3MqUWQZyO2TUQqP9VnT7CqXHF2laTFRy487+b6QZyotCazOySAuZLAvplCaOVsg1tVn/Zlmwfg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rollup@4.59.0:
|
||||
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -1331,6 +1600,9 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1379,6 +1651,10 @@ packages:
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyexec@1.0.2:
|
||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1411,6 +1687,36 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
tsdown@0.12.9:
|
||||
resolution: {integrity: sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@arethetypeswrong/core': ^0.18.1
|
||||
publint: ^0.3.0
|
||||
typescript: ^5.0.0
|
||||
unplugin-lightningcss: ^0.4.0
|
||||
unplugin-unused: ^0.5.0
|
||||
peerDependenciesMeta:
|
||||
'@arethetypeswrong/core':
|
||||
optional: true
|
||||
publint:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
unplugin-lightningcss:
|
||||
optional: true
|
||||
unplugin-unused:
|
||||
optional: true
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
turbo-darwin-64@2.8.14:
|
||||
resolution: {integrity: sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw==}
|
||||
cpu: [x64]
|
||||
@@ -1454,6 +1760,12 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
unconfig-core@7.5.0:
|
||||
resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
|
||||
|
||||
unconfig@7.5.0:
|
||||
resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
@@ -1616,18 +1928,47 @@ packages:
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
yaml@2.8.2:
|
||||
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/generator@7.29.1':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@changesets/apply-release-plan@7.1.0':
|
||||
dependencies:
|
||||
'@changesets/config': 3.1.3
|
||||
@@ -1771,6 +2112,33 @@ snapshots:
|
||||
human-id: 4.1.3
|
||||
prettier: 2.8.8
|
||||
|
||||
'@clack/core@0.4.1':
|
||||
dependencies:
|
||||
picocolors: 1.1.1
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@clack/prompts@0.9.1':
|
||||
dependencies:
|
||||
'@clack/core': 0.4.1
|
||||
picocolors: 1.1.1
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@emnapi/core@1.8.1':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.1.0
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.8.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
@@ -1931,8 +2299,20 @@ snapshots:
|
||||
|
||||
'@ioredis/commands@1.5.1': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
@@ -1971,6 +2351,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.1':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.8.1
|
||||
'@emnapi/runtime': 1.8.1
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -1983,6 +2370,61 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.20.1
|
||||
|
||||
'@oxc-project/types@0.115.0': {}
|
||||
|
||||
'@quansync/fs@1.0.0':
|
||||
dependencies:
|
||||
quansync: 1.0.0
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.7':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.7':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
optional: true
|
||||
|
||||
@@ -2058,6 +2500,11 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
@@ -2096,13 +2543,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15))':
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@22.19.15)
|
||||
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
dependencies:
|
||||
@@ -2175,6 +2622,8 @@ snapshots:
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansis@4.2.0: {}
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@@ -2185,10 +2634,17 @@ snapshots:
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-kit@2.2.0:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
pathe: 2.0.3
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
dependencies:
|
||||
is-windows: 1.0.2
|
||||
|
||||
birpc@2.9.0: {}
|
||||
|
||||
body-parser@2.2.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -2233,8 +2689,14 @@ snapshots:
|
||||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
content-disposition@1.0.1: {}
|
||||
@@ -2262,16 +2724,22 @@ snapshots:
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
detect-indent@6.1.0: {}
|
||||
|
||||
diff@8.0.3: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
dts-resolver@2.1.3: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -2280,6 +2748,8 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
empathic@2.0.0: {}
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
enquirer@2.4.1:
|
||||
@@ -2489,6 +2959,10 @@ snapshots:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -2514,6 +2988,8 @@ snapshots:
|
||||
|
||||
hono@4.12.5: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@@ -2568,6 +3044,8 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jose@6.2.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
@@ -2581,6 +3059,8 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema-typed@8.0.2: {}
|
||||
@@ -2713,6 +3193,8 @@ snapshots:
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
quansync@1.0.0: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
@@ -2731,6 +3213,8 @@ snapshots:
|
||||
pify: 4.0.1
|
||||
strip-bom: 3.0.0
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
redis-errors@1.2.0: {}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
@@ -2741,8 +3225,48 @@ snapshots:
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rolldown-plugin-dts@0.13.14(rolldown@1.0.0-rc.7)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.1
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
ast-kit: 2.2.0
|
||||
birpc: 2.9.0
|
||||
debug: 4.4.3
|
||||
dts-resolver: 2.1.3
|
||||
get-tsconfig: 4.13.6
|
||||
rolldown: 1.0.0-rc.7
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- oxc-resolver
|
||||
- supports-color
|
||||
|
||||
rolldown@1.0.0-rc.7:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.115.0
|
||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm64': 1.0.0-rc.7
|
||||
'@rolldown/binding-darwin-arm64': 1.0.0-rc.7
|
||||
'@rolldown/binding-darwin-x64': 1.0.0-rc.7
|
||||
'@rolldown/binding-freebsd-x64': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.7
|
||||
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.7
|
||||
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.7
|
||||
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.7
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.7
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.7
|
||||
|
||||
rollup@4.59.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -2857,6 +3381,8 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
@@ -2892,6 +3418,8 @@ snapshots:
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -2913,6 +3441,39 @@ snapshots:
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
tsdown@0.12.9(typescript@5.9.3):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
cac: 6.7.14
|
||||
chokidar: 4.0.3
|
||||
debug: 4.4.3
|
||||
diff: 8.0.3
|
||||
empathic: 2.0.0
|
||||
hookable: 5.5.3
|
||||
rolldown: 1.0.0-rc.7
|
||||
rolldown-plugin-dts: 0.13.14(rolldown@1.0.0-rc.7)(typescript@5.9.3)
|
||||
semver: 7.7.4
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
unconfig: 7.5.0
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- '@typescript/native-preview'
|
||||
- oxc-resolver
|
||||
- supports-color
|
||||
- vue-tsc
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
get-tsconfig: 4.13.6
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
turbo-darwin-64@2.8.14:
|
||||
optional: true
|
||||
|
||||
@@ -2948,6 +3509,19 @@ snapshots:
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
unconfig-core@7.5.0:
|
||||
dependencies:
|
||||
'@quansync/fs': 1.0.0
|
||||
quansync: 1.0.0
|
||||
|
||||
unconfig@7.5.0:
|
||||
dependencies:
|
||||
'@quansync/fs': 1.0.0
|
||||
defu: 6.1.4
|
||||
jiti: 2.6.1
|
||||
quansync: 1.0.0
|
||||
unconfig-core: 7.5.0
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
universalify@0.1.2: {}
|
||||
@@ -2974,13 +3548,13 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.15):
|
||||
vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.1(@types/node@22.19.15)
|
||||
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -3004,7 +3578,7 @@ snapshots:
|
||||
'@types/node': 22.19.15
|
||||
fsevents: 2.3.3
|
||||
|
||||
vite@7.3.1(@types/node@22.19.15):
|
||||
vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -3015,6 +3589,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vitest@2.1.9(@types/node@22.19.15):
|
||||
dependencies:
|
||||
@@ -3051,11 +3628,11 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@3.2.4(@types/node@22.19.15):
|
||||
vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15))
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -3073,8 +3650,8 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.3.1(@types/node@22.19.15)
|
||||
vite-node: 3.2.4(@types/node@22.19.15)
|
||||
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
@@ -3103,8 +3680,12 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
Reference in New Issue
Block a user