Compare commits
12 Commits
feat/insta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f03c05523 | |||
| c3f810bbd1 | |||
| b2cbf898d7 | |||
| b2cec8c6ba | |||
| 81c1775a03 | |||
| f64ec12f39 | |||
| 026382325c | |||
| 1bfd8570d6 | |||
| 312acd8bad | |||
| d08b969918 | |||
| 051de0d8a9 | |||
| bd76df1a50 |
@@ -103,12 +103,12 @@ steps:
|
|||||||
- mkdir -p /kaniko/.docker
|
- mkdir -p /kaniko/.docker
|
||||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||||
- |
|
- |
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
||||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:latest"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:latest"
|
||||||
fi
|
fi
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:$CI_COMMIT_TAG"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:$CI_COMMIT_TAG"
|
||||||
fi
|
fi
|
||||||
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
|
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -128,12 +128,12 @@ steps:
|
|||||||
- mkdir -p /kaniko/.docker
|
- mkdir -p /kaniko/.docker
|
||||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||||
- |
|
- |
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:latest"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
||||||
fi
|
fi
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:$CI_COMMIT_TAG"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
||||||
fi
|
fi
|
||||||
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
|
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
16
AGENTS.md
16
AGENTS.md
@@ -58,14 +58,14 @@ pnpm typecheck && pnpm lint && pnpm format:check # Quality gates
|
|||||||
|
|
||||||
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
|
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
|
||||||
|
|
||||||
| Value | When to use | Budget |
|
| Value | When to use | Budget |
|
||||||
| -------- | ----------------------------------------------------------- | -------------------------- |
|
| --------- | ----------------------------------------------------------- | -------------------------- |
|
||||||
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
|
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
|
||||||
| `glm-5` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
|
| `glm-5.1` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
|
||||||
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
|
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
|
||||||
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
|
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
|
||||||
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
|
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
|
||||||
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
|
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
|
||||||
|
|
||||||
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
|
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -7,7 +7,13 @@ Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi —
|
|||||||
## Quick Install
|
## Quick Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
|
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
|
||||||
@@ -179,8 +185,8 @@ Consent state is persisted in config. Remote upload is a no-op until you run `mo
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@git.mosaicstack.dev:mosaicstack/mosaic-stack.git
|
git clone git@git.mosaicstack.dev:mosaicstack/stack.git
|
||||||
cd mosaic-stack
|
cd stack
|
||||||
|
|
||||||
# Start infrastructure (Postgres, Valkey, Jaeger)
|
# Start infrastructure (Postgres, Valkey, Jaeger)
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
@@ -229,7 +235,7 @@ npm packages are published to the Gitea package registry on main merges.
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
mosaic-stack/
|
stack/
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── gateway/ NestJS API + WebSocket hub (Fastify, Socket.IO, OTEL)
|
│ ├── gateway/ NestJS API + WebSocket hub (Fastify, Socket.IO, OTEL)
|
||||||
│ └── web/ Next.js dashboard (React 19, Tailwind)
|
│ └── web/ Next.js dashboard (React 19, Tailwind)
|
||||||
@@ -302,7 +308,13 @@ Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding),
|
|||||||
Run the installer again — it handles upgrades automatically:
|
Run the installer again — it handles upgrades automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the CLI:
|
Or use the CLI:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "apps/gateway"
|
"directory": "apps/gateway"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -165,7 +165,13 @@ The `mosaic` CLI provides a terminal interface to the same gateway API.
|
|||||||
Install via the Mosaic installer:
|
Install via the Mosaic installer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer places the `mosaic` binary at `~/.npm-global/bin/mosaic`. Flags for
|
The installer places the `mosaic` binary at `~/.npm-global/bin/mosaic`. Flags for
|
||||||
|
|||||||
227
docs/scratchpads/iuv-m03-design.md
Normal file
227
docs/scratchpads/iuv-m03-design.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# IUV-M03 Design: Provider-first intelligent flow + drill-down main menu
|
||||||
|
|
||||||
|
**Issue:** #438
|
||||||
|
**Branch:** `feat/install-ux-intent`
|
||||||
|
**Date:** 2026-04-05
|
||||||
|
|
||||||
|
## 1. New first-run state machine
|
||||||
|
|
||||||
|
The linear 12-stage interrogation is replaced with a menu-driven architecture.
|
||||||
|
|
||||||
|
### Flow overview
|
||||||
|
|
||||||
|
```
|
||||||
|
Welcome banner
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Detect existing install (auto)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Main Menu (loop)
|
||||||
|
|-- Quick Start -> provider key + admin creds -> finalize
|
||||||
|
|-- Providers -> LLM API key config
|
||||||
|
|-- Agent Identity -> intent intake + naming (deterministic)
|
||||||
|
|-- Skills -> recommended / custom selection
|
||||||
|
|-- Gateway -> port, storage tier, hostname, CORS
|
||||||
|
|-- Advanced -> SOUL.md, USER.md, TOOLS.md, runtimes, hooks
|
||||||
|
|-- Finish & Apply -> finalize + gateway bootstrap
|
||||||
|
v
|
||||||
|
Done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menu navigation
|
||||||
|
|
||||||
|
- Main menu is a `select` prompt. Each option drills into a sub-flow.
|
||||||
|
- Completing a section returns to the main menu.
|
||||||
|
- Menu items show completion state: `[done]` hint after configuration.
|
||||||
|
- `Finish & Apply` is always last and requires at minimum a provider key (or explicit skip).
|
||||||
|
- The menu tracks configured sections in `WizardState.completedSections`.
|
||||||
|
|
||||||
|
### Headless bypass
|
||||||
|
|
||||||
|
When `MOSAIC_ASSUME_YES=1` or `!process.stdin.isTTY`, the entire menu is skipped.
|
||||||
|
The wizard runs: defaults + env var overrides -> finalize -> gateway config -> bootstrap.
|
||||||
|
This preserves full backward compatibility with `tools/install.sh --yes`.
|
||||||
|
|
||||||
|
## 2. Quick Start path
|
||||||
|
|
||||||
|
Target: 3-5 questions max. Under 90 seconds for a returning user.
|
||||||
|
|
||||||
|
### Questions asked
|
||||||
|
|
||||||
|
1. **Provider API key** (Anthropic/OpenAI) - `text` prompt with paste support
|
||||||
|
2. **Admin email** - `text` prompt
|
||||||
|
3. **Admin password** - masked + confirmed
|
||||||
|
|
||||||
|
### Questions skipped (with defaults)
|
||||||
|
|
||||||
|
| Setting | Default | Rationale |
|
||||||
|
| ---------------------------- | ------------------------------- | ---------------------- |
|
||||||
|
| Agent name | "Mosaic" | Generic but branded |
|
||||||
|
| Port | 14242 | Standard default |
|
||||||
|
| Storage tier | local | No external deps |
|
||||||
|
| Hostname | localhost | Dev-first |
|
||||||
|
| CORS origin | http://localhost:3000 | Standard web UI port |
|
||||||
|
| Skills | recommended set | Curated by maintainers |
|
||||||
|
| Runtimes | auto-detected | No user input needed |
|
||||||
|
| Communication style | direct | Most popular choice |
|
||||||
|
| SOUL.md / USER.md / TOOLS.md | template defaults | Can customize later |
|
||||||
|
| Hooks | auto-install if Claude detected | Safe default |
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Quick Start selected
|
||||||
|
-> "Paste your LLM API key (Anthropic recommended):"
|
||||||
|
-> [auto-detect provider from key prefix: sk-ant-* = Anthropic, sk-* = OpenAI]
|
||||||
|
-> Apply all defaults
|
||||||
|
-> Run finalize (sync framework, write configs, link assets, sync skills)
|
||||||
|
-> Run gateway config (headless-style with defaults + provided key)
|
||||||
|
-> "Admin email:"
|
||||||
|
-> "Admin password:" (masked + confirm)
|
||||||
|
-> Run gateway bootstrap
|
||||||
|
-> Done
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Provider-first flow
|
||||||
|
|
||||||
|
Provider configuration (currently buried in gateway-config stage as "ANTHROPIC_API_KEY")
|
||||||
|
moves to a dedicated top-level menu item and is the first question in Quick Start.
|
||||||
|
|
||||||
|
### Provider detection
|
||||||
|
|
||||||
|
The API key prefix determines the provider:
|
||||||
|
|
||||||
|
- `sk-ant-api03-*` -> Anthropic (Claude)
|
||||||
|
- `sk-*` -> OpenAI
|
||||||
|
- Empty/skipped -> no provider (gateway starts without LLM access)
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
The provider key is stored in the gateway `.env` as `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`.
|
||||||
|
For Quick Start, this replaces the old interactive prompt in `collectAndWriteConfig`.
|
||||||
|
|
||||||
|
### Menu section: "Providers"
|
||||||
|
|
||||||
|
In the drill-down menu, "Providers" lets users:
|
||||||
|
|
||||||
|
1. Enter/change their API key
|
||||||
|
2. See which provider was detected
|
||||||
|
3. Optionally configure a second provider
|
||||||
|
|
||||||
|
For v0.0.27, we support Anthropic and OpenAI keys only. The key is stored
|
||||||
|
in `WizardState` and written during finalize.
|
||||||
|
|
||||||
|
## 4. Intent intake + naming (deterministic fallback - Option B)
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
At install time, the LLM provider may not be configured yet (chicken-and-egg).
|
||||||
|
We use **Option B: deterministic advisor** for the install wizard.
|
||||||
|
|
||||||
|
### Flow (Agent Identity menu section)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. "What will this agent primarily help you with?"
|
||||||
|
-> Select from presets:
|
||||||
|
- General purpose assistant
|
||||||
|
- Software development
|
||||||
|
- DevOps & infrastructure
|
||||||
|
- Research & analysis
|
||||||
|
- Content & writing
|
||||||
|
- Custom (free text description)
|
||||||
|
|
||||||
|
2. System proposes a thematic name based on selection:
|
||||||
|
- General purpose -> "Mosaic"
|
||||||
|
- Software development -> "Forge"
|
||||||
|
- DevOps & infrastructure -> "Sentinel"
|
||||||
|
- Research & analysis -> "Atlas"
|
||||||
|
- Content & writing -> "Muse"
|
||||||
|
- Custom -> "Mosaic" (default)
|
||||||
|
|
||||||
|
3. "Your agent will be named 'Forge'. Press Enter to accept or type a new name:"
|
||||||
|
-> User confirms or overrides
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
- Agent name -> `WizardState.soul.agentName` -> written to SOUL.md
|
||||||
|
- Intent category -> `WizardState.agentIntent` (new field) -> written to `~/.config/mosaic/agent.json`
|
||||||
|
|
||||||
|
### Post-install LLM-powered intake (future)
|
||||||
|
|
||||||
|
A future `mosaic configure identity` command can use the configured LLM to:
|
||||||
|
|
||||||
|
- Accept free-text intent description
|
||||||
|
- Generate an expounded persona
|
||||||
|
- Propose a contextual name
|
||||||
|
|
||||||
|
This is explicitly out of scope for the install wizard.
|
||||||
|
|
||||||
|
## 5. Headless backward-compat
|
||||||
|
|
||||||
|
### Supported env vars (unchanged)
|
||||||
|
|
||||||
|
| Variable | Used by |
|
||||||
|
| -------------------------- | ---------------------------------------------- |
|
||||||
|
| `MOSAIC_ASSUME_YES=1` | Skip all prompts, use defaults + env overrides |
|
||||||
|
| `MOSAIC_ADMIN_NAME` | Gateway bootstrap |
|
||||||
|
| `MOSAIC_ADMIN_EMAIL` | Gateway bootstrap |
|
||||||
|
| `MOSAIC_ADMIN_PASSWORD` | Gateway bootstrap |
|
||||||
|
| `MOSAIC_GATEWAY_PORT` | Gateway config |
|
||||||
|
| `MOSAIC_HOSTNAME` | Gateway config (CORS derivation) |
|
||||||
|
| `MOSAIC_CORS_ORIGIN` | Gateway config (full override) |
|
||||||
|
| `MOSAIC_STORAGE_TIER` | Gateway config (local/team) |
|
||||||
|
| `MOSAIC_DATABASE_URL` | Gateway config (team tier) |
|
||||||
|
| `MOSAIC_VALKEY_URL` | Gateway config (team tier) |
|
||||||
|
| `MOSAIC_ANTHROPIC_API_KEY` | Provider config |
|
||||||
|
|
||||||
|
### New env vars
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
| --------------------- | ----------------------------------------- |
|
||||||
|
| `MOSAIC_AGENT_NAME` | Override agent name in headless mode |
|
||||||
|
| `MOSAIC_AGENT_INTENT` | Override intent category in headless mode |
|
||||||
|
|
||||||
|
### `tools/install.sh --yes`
|
||||||
|
|
||||||
|
The install script sets `MOSAIC_ASSUME_YES=1` and passes through env vars.
|
||||||
|
No changes needed to the script itself. The new wizard detects headless mode
|
||||||
|
at the top of `runWizard` and runs a linear path identical to the old flow.
|
||||||
|
|
||||||
|
## 6. Explicit non-goals
|
||||||
|
|
||||||
|
- **No GUI** — this is a terminal wizard only
|
||||||
|
- **No multi-user install** — single-user, single-machine
|
||||||
|
- **No registry changes** — npm publish flow is unchanged
|
||||||
|
- **No LLM calls during install** — deterministic fallback only
|
||||||
|
- **No new dependencies** — uses existing @clack/prompts and picocolors
|
||||||
|
- **No changes to gateway API** — only the wizard orchestration changes
|
||||||
|
- **No changes to tools/install.sh** — headless compat maintained via env vars
|
||||||
|
|
||||||
|
## 7. Implementation plan
|
||||||
|
|
||||||
|
### Files to modify
|
||||||
|
|
||||||
|
1. `packages/mosaic/src/types.ts` — add `MenuSection`, `AgentIntent`, `completedSections`, `agentIntent`, `providerKey`, `providerType` to WizardState
|
||||||
|
2. `packages/mosaic/src/wizard.ts` — replace linear flow with menu loop
|
||||||
|
3. `packages/mosaic/src/stages/mode-select.ts` — becomes the main menu
|
||||||
|
4. `packages/mosaic/src/stages/provider-setup.ts` — new: provider key collection
|
||||||
|
5. `packages/mosaic/src/stages/agent-intent.ts` — new: intent intake + naming
|
||||||
|
6. `packages/mosaic/src/stages/menu-gateway.ts` — new: gateway sub-menu wrapper
|
||||||
|
7. `packages/mosaic/src/stages/quick-start.ts` — new: quick start linear path
|
||||||
|
8. `packages/mosaic/src/constants.ts` — add intent presets and name mappings
|
||||||
|
9. `packages/mosaic/package.json` — version bump 0.0.26 -> 0.0.27
|
||||||
|
|
||||||
|
### Files to add (tests)
|
||||||
|
|
||||||
|
1. `packages/mosaic/src/stages/wizard-menu.spec.ts` — menu navigation tests
|
||||||
|
2. `packages/mosaic/src/stages/quick-start.spec.ts` — quick start path tests
|
||||||
|
3. `packages/mosaic/src/stages/agent-intent.spec.ts` — intent + naming tests
|
||||||
|
4. `packages/mosaic/src/stages/provider-setup.spec.ts` — provider detection tests
|
||||||
|
|
||||||
|
### Migration strategy
|
||||||
|
|
||||||
|
The existing stage functions remain intact. The menu system wraps them —
|
||||||
|
each menu item calls the appropriate stage function(s). The linear headless
|
||||||
|
path calls them in the same order as before.
|
||||||
110
docs/scratchpads/tools-md-seeding-20260411.md
Normal file
110
docs/scratchpads/tools-md-seeding-20260411.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Hotfix Scratchpad — `install.sh` does not seed `TOOLS.md`
|
||||||
|
|
||||||
|
- **Issue:** mosaicstack/stack#457
|
||||||
|
- **Branch:** `fix/tools-md-seeding`
|
||||||
|
- **Type:** Out-of-mission hotfix (not part of Install UX v2 mission)
|
||||||
|
- **Started:** 2026-04-11
|
||||||
|
- **Ships in:** `@mosaicstack/mosaic` 0.0.30
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Ensure `~/.config/mosaic/TOOLS.md` is created on every supported install path so the mandatory AGENTS.md load order actually resolves. The load order lists `TOOLS.md` at position 5 but the bash installer never seeds it.
|
||||||
|
|
||||||
|
## Root cause
|
||||||
|
|
||||||
|
`packages/mosaic/framework/install.sh:228-236` — the post-sync "Seed defaults" loop explicitly lists `AGENTS.md STANDARDS.md`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEFAULTS_DIR="$TARGET_DIR/defaults"
|
||||||
|
if [[ -d "$DEFAULTS_DIR" ]]; then
|
||||||
|
for default_file in AGENTS.md STANDARDS.md; do # ← missing TOOLS.md
|
||||||
|
if [[ -f "$DEFAULTS_DIR/$default_file" ]] && [[ ! -f "$TARGET_DIR/$default_file" ]]; then
|
||||||
|
cp "$DEFAULTS_DIR/$default_file" "$TARGET_DIR/$default_file"
|
||||||
|
ok "Seeded $default_file from defaults"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
`TOOLS.md` is listed in `PRESERVE_PATHS` (line 24) but never created in the first place. A fresh bootstrap install via `tools/install.sh → framework/install.sh` leaves `~/.config/mosaic/TOOLS.md` absent, and the agent load order then points at a missing file.
|
||||||
|
|
||||||
|
### Secondary: TypeScript `syncFramework` is too greedy
|
||||||
|
|
||||||
|
`packages/mosaic/src/config/file-adapter.ts:133-160` — `FileConfigAdapter.syncFramework` correctly seeds TOOLS.md, but it does so by iterating _every_ file in `framework/defaults/`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
for (const entry of readdirSync(defaultsDir)) {
|
||||||
|
const dest = join(this.mosaicHome, entry);
|
||||||
|
if (!existsSync(dest)) {
|
||||||
|
copyFileSync(join(defaultsDir, entry), dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`framework/defaults/` contains:
|
||||||
|
|
||||||
|
```
|
||||||
|
AGENTS.md
|
||||||
|
AUDIT-2026-02-17-framework-consistency.md
|
||||||
|
README.md
|
||||||
|
SOUL.md ← hardcoded "Jarvis"
|
||||||
|
STANDARDS.md
|
||||||
|
TOOLS.md
|
||||||
|
USER.md
|
||||||
|
```
|
||||||
|
|
||||||
|
So on a fresh install the TS wizard would silently copy the `Jarvis`-flavored `SOUL.md` + placeholder `USER.md` + internal `AUDIT-*.md` and `README.md` into the user's mosaic home before `mosaic init` ever prompts them. That's a latent identity bug as well as a root-clutter bug — the wizard's own stages are responsible for generating `SOUL.md`/`USER.md` via templates.
|
||||||
|
|
||||||
|
### Tertiary: stale `TOOLS.md.template`
|
||||||
|
|
||||||
|
`packages/mosaic/framework/templates/TOOLS.md.template` still references `~/.config/mosaic/rails/git/…` and `~/.config/mosaic/rails/codex/…`. The `rails/` tree was renamed to `tools/` in the v1→v2 migration (see `run_migrations` in `install.sh`, which removes the old `rails/` symlink). Any user who does run `mosaic init` ends up with a `TOOLS.md` that points to paths that no longer exist.
|
||||||
|
|
||||||
|
## Scope of this fix
|
||||||
|
|
||||||
|
1. **`packages/mosaic/framework/install.sh`** — extend the explicit seed list to include `TOOLS.md`.
|
||||||
|
2. **`packages/mosaic/src/config/file-adapter.ts`** — restrict `syncFramework` defaults-seeding to an explicit whitelist (`AGENTS.md`, `STANDARDS.md`, `TOOLS.md`) so the TS wizard never accidentally seeds `SOUL.md`/`USER.md`/`README.md`/`AUDIT-*.md` into the mosaic home.
|
||||||
|
3. **`packages/mosaic/framework/templates/TOOLS.md.template`** — replace `rails/` with `tools/` in the wrapper-path examples (minimal surgical fix; full template modernization is out of scope for a 0.0.30 hotfix).
|
||||||
|
4. **Regression test** — unit test around `FileConfigAdapter.syncFramework` that runs against a tmpdir fixture asserting:
|
||||||
|
- `TOOLS.md` is seeded when absent
|
||||||
|
- `AGENTS.md` / `STANDARDS.md` are still seeded when absent
|
||||||
|
- `SOUL.md` / `USER.md` are **not** seeded from `defaults/` (the wizard stages own those)
|
||||||
|
- Existing root files are not clobbered.
|
||||||
|
|
||||||
|
Out of scope (tracked separately / future work):
|
||||||
|
|
||||||
|
- Regenerating `defaults/SOUL.md` and `defaults/USER.md` so they no longer contain Jarvis-specific content.
|
||||||
|
- Fully modernizing `TOOLS.md.template` to match the rich canonical `defaults/TOOLS.md` reference.
|
||||||
|
- `issue-create.sh` / `pr-create.sh` `eval` bugs (already captured to OpenBrain from the prior hotfix).
|
||||||
|
|
||||||
|
## Plan / checklist
|
||||||
|
|
||||||
|
- [ ] Branch `fix/tools-md-seeding` from `main` (at `b2cbf89`)
|
||||||
|
- [ ] File Gitea issue (direct API; wrappers broken for bodies with backticks)
|
||||||
|
- [ ] Scratchpad created (this file)
|
||||||
|
- [ ] `install.sh` seed loop extended to `AGENTS.md STANDARDS.md TOOLS.md`
|
||||||
|
- [ ] `file-adapter.ts` seeding restricted to explicit whitelist
|
||||||
|
- [ ] `TOOLS.md.template` `rails/` → `tools/`
|
||||||
|
- [ ] Regression test added (`file-adapter.test.ts`) — failing first, then green
|
||||||
|
- [ ] `pnpm --filter @mosaicstack/mosaic run typecheck` green
|
||||||
|
- [ ] `pnpm --filter @mosaicstack/mosaic run lint` green
|
||||||
|
- [ ] `pnpm --filter @mosaicstack/mosaic exec vitest run` — new test green, no new failures beyond the known pre-existing `uninstall.spec.ts:138`
|
||||||
|
- [ ] Repo baselines: `pnpm typecheck` / `pnpm lint` / `pnpm format:check`
|
||||||
|
- [ ] Independent code review (`feature-dev:code-reviewer`, sonnet tier)
|
||||||
|
- [ ] Commit + push
|
||||||
|
- [ ] PR opened via Gitea API
|
||||||
|
- [ ] CI queue guard cleared (bypass local `ci-queue-wait.sh` if stale origin URL breaks it; query Gitea API directly)
|
||||||
|
- [ ] CI green on PR
|
||||||
|
- [ ] PR merged (squash)
|
||||||
|
- [ ] CI green on main
|
||||||
|
- [ ] Issue closed with link to merge commit
|
||||||
|
- [ ] `chore/release-mosaic-0.0.30` branch bumps `packages/mosaic/package.json` 0.0.29 → 0.0.30
|
||||||
|
- [ ] Release PR opened + merged
|
||||||
|
- [ ] `.woodpecker/publish.yml` auto-publishes to Gitea npm registry
|
||||||
|
- [ ] Publish verified (`npm view @mosaicstack/mosaic version` or registry check)
|
||||||
|
|
||||||
|
## Risks / blockers
|
||||||
|
|
||||||
|
- `ci-queue-wait.sh` wrapper may still crash on stale `origin` URL (captured in OpenBrain from prior hotfix). Workaround: query Gitea API directly for running/queued pipelines.
|
||||||
|
- `issue-create.sh` / `pr-create.sh` `eval` bugs. Workaround: Gitea API direct call.
|
||||||
|
- `uninstall.spec.ts:138` is a pre-existing failure on main; not this change's problem.
|
||||||
|
- Publish flow is fire-and-forget on main push — if `publish.yml` fails, rollback means republishing a follow-up patch, not reverting the version bump.
|
||||||
114
docs/scratchpads/yolo-runtime-initial-arg-20260411.md
Normal file
114
docs/scratchpads/yolo-runtime-initial-arg-20260411.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Hotfix Scratchpad — `mosaic yolo <runtime>` passes runtime name as initial user message
|
||||||
|
|
||||||
|
- **Issue:** mosaicstack/stack#454
|
||||||
|
- **Branch:** `fix/yolo-runtime-initial-arg`
|
||||||
|
- **Type:** Out-of-mission hotfix (not part of Install UX v2 mission)
|
||||||
|
- **Started:** 2026-04-11
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Stop `mosaic yolo <runtime>` from passing the runtime name (`claude`, `codex`, etc.) as the initial user message to the underlying CLI. Restore the mission-auto-prompt path for yolo launches.
|
||||||
|
|
||||||
|
## Root cause (confirmed)
|
||||||
|
|
||||||
|
`packages/mosaic/src/commands/launch.ts:779` — the `yolo <runtime>` action handler:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.action((runtime: string, _opts: unknown, cmd: Command) => {
|
||||||
|
// ... validate runtime ...
|
||||||
|
launchRuntime(runtime as RuntimeName, cmd.args, true);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Commander.js includes declared positional arguments in `cmd.args`. For `mosaic yolo claude`:
|
||||||
|
|
||||||
|
- `runtime` (destructured) = `"claude"`
|
||||||
|
- `cmd.args` = `["claude"]` — the same value
|
||||||
|
|
||||||
|
`launchRuntime` treats `["claude"]` as excess positional args, and for the `claude` case that becomes the initial user message. As a secondary consequence, `hasMissionNoArgs` evaluates false, so the mission-auto-prompt path is bypassed too.
|
||||||
|
|
||||||
|
## Live reproduction (intercepted claude binary)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ PATH=/tmp/fake-claude-bin:$PATH mosaic yolo claude
|
||||||
|
[mosaic] Launching Claude Code in YOLO mode...
|
||||||
|
argv[1]: --dangerously-skip-permissions
|
||||||
|
argv[2]: --append-system-prompt
|
||||||
|
argv[3] (len=25601): # ACTIVE MISSION — HARD GATE ...
|
||||||
|
argv[4]: claude ← the bug
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-yolo variant `mosaic claude` is clean:
|
||||||
|
|
||||||
|
```
|
||||||
|
argv[1]: --append-system-prompt
|
||||||
|
argv[2]: <prompt>
|
||||||
|
argv[3]: Active mission detected: MVP. Read the mission state files and report status.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Refactor `launch.ts`: extract `registerRuntimeLaunchers(program, handler)` with an injectable handler so commander wiring is testable without spawning subprocesses. `registerLaunchCommands` delegates to it with `launchRuntime` as the handler.
|
||||||
|
2. Fix: in the `yolo <runtime>` action, pass `cmd.args.slice(1)` instead of `cmd.args`.
|
||||||
|
3. Add `packages/mosaic/src/commands/launch.spec.ts`:
|
||||||
|
- Failing-first reproducer: parse `['node','x','yolo','claude']` and assert handler receives `extraArgs=[]` and `yolo=true`.
|
||||||
|
- Regression test: parse `['node','x','claude']` asserts handler receives `extraArgs=[]` and `yolo=false`.
|
||||||
|
- Excess args: parse `['node','x','yolo','claude','--print','hi']` asserts handler receives `extraArgs=['--print','hi']` (with `--print` kept because `allowUnknownOption` is true).
|
||||||
|
- Excess args non-yolo: parse `['node','x','claude','--print','hi']` asserts `extraArgs=['--print','hi']`.
|
||||||
|
- Reject unknown runtime under yolo.
|
||||||
|
4. Run typecheck, lint, format:check, vitest for `@mosaicstack/mosaic`.
|
||||||
|
5. Independent code review (feature-dev:code-reviewer subagent, sonnet tier).
|
||||||
|
6. Commit → push → PR via wrappers → merge → CI green → close issue #454.
|
||||||
|
7. Release decision (`mosaic-v0.0.30`) deferred to Jason after merge.
|
||||||
|
|
||||||
|
## Framework compliance sub-findings (out-of-scope; to capture in OpenBrain after)
|
||||||
|
|
||||||
|
- `~/.config/mosaic/tools/git/issue-create.sh` uses `eval` on `$BODY`; arbitrary bodies with backticks, `$`, or parens break catastrophically.
|
||||||
|
- `gitea_issue_create_api` fallback uses `curl -fsS` without `-L`; after the `mosaicstack/mosaic-stack → mosaicstack/stack` rename, the API redirect is not followed and the fallback silently fails.
|
||||||
|
- Local repo `origin` remote still points at old `mosaic/mosaic-stack.git` slug. Not touched here per git-config safety rule.
|
||||||
|
- `~/.config/mosaic/TOOLS.md` referenced by the global load order but does not exist on disk.
|
||||||
|
|
||||||
|
These will be captured to OpenBrain after the hotfix merges so they don't get lost, and filed as separate tracking items.
|
||||||
|
|
||||||
|
## Progress checkpoints
|
||||||
|
|
||||||
|
- [x] Branch created (`fix/yolo-runtime-initial-arg`)
|
||||||
|
- [x] Issue #454 opened
|
||||||
|
- [x] Scratchpad scaffolded
|
||||||
|
- [x] Failing test added (red)
|
||||||
|
- [x] Refactor + fix applied
|
||||||
|
- [x] Tests green (launch.spec.ts 11/11)
|
||||||
|
- [x] Baselines green (typecheck, lint, format:check, vitest — pre-existing `uninstall.spec.ts:138` failure on branch main acknowledged, not caused by this change)
|
||||||
|
- [x] Code review pass (feature-dev:code-reviewer, sonnet — no blockers)
|
||||||
|
- [x] Commit + push (commit 1dd4f59)
|
||||||
|
- [x] PR opened (mosaicstack/stack#455)
|
||||||
|
- [x] CI queue guard cleared (no pending pipelines pre-push or pre-merge)
|
||||||
|
- [x] PR merged (squash merge commit b2cec8c6bac29336a6cdcdb4f19806f7b5fa0054)
|
||||||
|
- [x] CI green on main (`ci/woodpecker/push/ci` + `ci/woodpecker/push/publish` both success on merge commit)
|
||||||
|
- [x] Issue #454 closed
|
||||||
|
- [x] Scratchpad final evidence entry
|
||||||
|
|
||||||
|
## Tests run
|
||||||
|
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic run typecheck` → green
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic run lint` → green
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic exec prettier --check "src/**/*.ts"` → green
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/launch.spec.ts` → 11/11 pass
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic exec vitest run` → 270/271 pass (1 pre-existing `uninstall.spec.ts:138` EACCES failure, confirmed on the branch before this change)
|
||||||
|
- `pnpm typecheck` (repo) → green
|
||||||
|
- `pnpm lint` (repo) → green
|
||||||
|
- `pnpm format:check` (repo) → green (after prettier-writing the scratchpad)
|
||||||
|
|
||||||
|
## Risks / blockers
|
||||||
|
|
||||||
|
None expected. Refactor is small and the Commander API is stable. Test needs `exitOverride()` to prevent `process.exit` on invalid runtime.
|
||||||
|
|
||||||
|
## Final verification evidence
|
||||||
|
|
||||||
|
- PR: mosaicstack/stack#455 — state `closed`, merged.
|
||||||
|
- Merge commit: `b2cec8c6bac29336a6cdcdb4f19806f7b5fa0054` (squash to `main`).
|
||||||
|
- Post-merge CI (main @ b2cec8c6): `ci/woodpecker/push/ci` = success, `ci/woodpecker/push/publish` = success. (`ci/woodpecker/tag/publish` was last observed as a pre-existing failure on the prior release tag and is unrelated to this change.)
|
||||||
|
- Issue mosaicstack/stack#454 closed with a comment linking the merge commit.
|
||||||
|
- Launch regression suite: `launch.spec.ts` 11/11 pass on main.
|
||||||
|
- Baselines on main after merge are inherited from the PR CI run.
|
||||||
|
- Release decision (`mosaicstack/mosaic` 0.0.30) intentionally deferred to the user — the fix is now sitting on main awaiting a release cut.
|
||||||
@@ -73,6 +73,27 @@ Spawn a worker instead. No exceptions. No "quick fixes."
|
|||||||
- Wait for at least one worker to complete before spawning more
|
- Wait for at least one worker to complete before spawning more
|
||||||
- This optimizes token usage and reduces context pressure
|
- This optimizes token usage and reduces context pressure
|
||||||
|
|
||||||
|
## File Ownership & Partitioning (Hard Rule for Parallel Workers)
|
||||||
|
|
||||||
|
When dispatching parallel workers, the orchestrator MUST assign **non-overlapping file scopes** to each worker. File collisions between parallel workers cause merge conflicts, lost edits, and wasted tokens.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. **Exclusive file ownership.** Each file may be assigned to at most one active worker. The orchestrator records ownership in the worker dispatch (prompt or task definition).
|
||||||
|
2. **Partition by directory or module.** Prefer assigning entire directories/modules to one worker rather than splitting files within a directory across workers.
|
||||||
|
3. **Shared files are serialized.** If two tasks must modify the same file (e.g., a shared types file, a barrel export), they MUST run sequentially — never in parallel. Mark the second task with `depends_on` pointing to the first.
|
||||||
|
4. **Test files follow source ownership.** If Worker A owns `src/auth/login.ts`, Worker A also owns `src/auth/__tests__/login.test.ts`. Do not split source and test across workers.
|
||||||
|
5. **Config files are orchestrator-reserved.** Files like `package.json`, `tsconfig.json`, and CI config are owned by the orchestrator and modified only between worker cycles, never during parallel execution.
|
||||||
|
6. **Document ownership in dispatch.** When spawning a worker, include an explicit `Files:` section listing owned paths/globs. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Files (exclusive — do not touch files outside this scope):
|
||||||
|
- apps/web/src/components/auth/**
|
||||||
|
- apps/web/src/lib/auth.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Conflict recovery.** If a worker edits a file outside its scope, the orchestrator MUST flag the violation, assess the diff, and either revert the out-of-scope change or re-run the affected worker with the corrected file.
|
||||||
|
|
||||||
## Delegation Mode Selection
|
## Delegation Mode Selection
|
||||||
|
|
||||||
Choose one delegation mode at session start:
|
Choose one delegation mode at session start:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/agent"
|
"directory": "packages/agent"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/auth"
|
"directory": "packages/auth"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/brain"
|
"directory": "packages/brain"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/config"
|
"directory": "packages/config"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/coord"
|
"directory": "packages/coord"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/db"
|
"directory": "packages/db"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/design-tokens"
|
"directory": "packages/design-tokens"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/forge"
|
"directory": "packages/forge"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/log"
|
"directory": "packages/log"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/macp"
|
"directory": "packages/macp"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/memory"
|
"directory": "packages/memory"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { runWizard } from '../../src/wizard.js';
|
|||||||
describe('Full Wizard (headless)', () => {
|
describe('Full Wizard (headless)', () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
const repoRoot = join(import.meta.dirname, '..', '..');
|
const repoRoot = join(import.meta.dirname, '..', '..');
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-wizard-test-'));
|
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-wizard-test-'));
|
||||||
@@ -32,12 +33,16 @@ describe('Full Wizard (headless)', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
rmSync(tmpDir, { recursive: true, force: true });
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
process.env = { ...originalEnv };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quick start produces valid SOUL.md', async () => {
|
it('quick start produces valid SOUL.md', async () => {
|
||||||
|
// The headless path reads agent name from MOSAIC_AGENT_NAME env var
|
||||||
|
// (via agentIntentStage) rather than prompting interactively.
|
||||||
|
process.env['MOSAIC_AGENT_NAME'] = 'TestBot';
|
||||||
|
|
||||||
const prompter = new HeadlessPrompter({
|
const prompter = new HeadlessPrompter({
|
||||||
'Installation mode': 'quick',
|
'Installation mode': 'quick',
|
||||||
'What name should agents use?': 'TestBot',
|
|
||||||
'Communication style': 'direct',
|
'Communication style': 'direct',
|
||||||
'Your name': 'Tester',
|
'Your name': 'Tester',
|
||||||
'Your pronouns': 'They/Them',
|
'Your pronouns': 'They/Them',
|
||||||
@@ -62,9 +67,10 @@ describe('Full Wizard (headless)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('quick start produces valid USER.md', async () => {
|
it('quick start produces valid USER.md', async () => {
|
||||||
|
process.env['MOSAIC_AGENT_NAME'] = 'TestBot';
|
||||||
|
|
||||||
const prompter = new HeadlessPrompter({
|
const prompter = new HeadlessPrompter({
|
||||||
'Installation mode': 'quick',
|
'Installation mode': 'quick',
|
||||||
'What name should agents use?': 'TestBot',
|
|
||||||
'Communication style': 'direct',
|
'Communication style': 'direct',
|
||||||
'Your name': 'Tester',
|
'Your name': 'Tester',
|
||||||
'Your pronouns': 'He/Him',
|
'Your pronouns': 'He/Him',
|
||||||
|
|||||||
@@ -151,11 +151,68 @@ When delegating work to subagents, you MUST select the cheapest model capable of
|
|||||||
|
|
||||||
**Runtime-specific syntax**: See the runtime reference for how to specify model tier when spawning subagents (e.g., Claude Code Task tool `model` parameter).
|
**Runtime-specific syntax**: See the runtime reference for how to specify model tier when spawning subagents (e.g., Claude Code Task tool `model` parameter).
|
||||||
|
|
||||||
|
## Superpowers Enforcement (Hard Rule)
|
||||||
|
|
||||||
|
Mosaic provides capabilities beyond basic code editing: **skills**, **hooks**, **MCP tools**, and **plugins**. These are not optional extras — they are force multipliers that agents MUST actively use when applicable. Under-utilization of superpowers is a framework violation.
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
Skills are domain-specific instruction sets in `~/.config/mosaic/skills/` that encode best practices, patterns, and guardrails. They are loaded into agents via the runtime's skill mechanism (e.g., Claude Code slash commands, Pi `--skill` flag).
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. Before starting implementation, scan available skills (`ls ~/.config/mosaic/skills/`) and load any that match the task domain.
|
||||||
|
2. When a skill exists for the technology being used (e.g., `nestjs-best-practices` for NestJS work), you MUST load it.
|
||||||
|
3. When spawning workers, include skill loading in the kickstart prompt.
|
||||||
|
4. If you complete a task without loading a relevant available skill, that is a quality gap.
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
Hooks provide automated quality gates (lint, format, typecheck) that fire on file edits. They are configured in the runtime settings and run automatically.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. Do NOT bypass or suppress hook output. If a hook reports errors, fix them before proceeding.
|
||||||
|
2. Hook failures are immediate feedback — treat them like failing tests.
|
||||||
|
3. If a hook is consistently failing on valid code, report it as a framework issue rather than working around it.
|
||||||
|
|
||||||
|
### MCP Tools
|
||||||
|
|
||||||
|
MCP servers extend agent capabilities with external integrations (sequential-thinking, web search, memory, browser automation, etc.). Available MCP tools are listed at session start.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. **sequential-thinking** is REQUIRED for planning, architecture, and multi-step reasoning. Use it — do not skip structured thinking for complex decisions.
|
||||||
|
2. **OpenBrain** (`capture`, `search`, `recent`) is the cross-agent memory layer. Capture discoveries and search for prior context at session start.
|
||||||
|
3. When a task involves web research, browser testing, or external data, use the available MCP tools (web-search, chrome-devtools, web-reader) rather than asking the user to look things up.
|
||||||
|
4. Check available MCP tools at session start and use them proactively throughout the session.
|
||||||
|
|
||||||
|
### Plugins (Runtime-Specific)
|
||||||
|
|
||||||
|
Runtime plugins (e.g., Claude Code's `feature-dev`, `pr-review-toolkit`, `code-review`) provide specialized agent capabilities like code review, architecture analysis, and test coverage analysis.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. After completing a significant code change, use code review plugins proactively — do not wait for the user to ask.
|
||||||
|
2. Before creating a PR, use PR review plugins to catch issues early.
|
||||||
|
3. When designing architecture, use planning/architecture plugins for structured analysis.
|
||||||
|
|
||||||
|
### Self-Evolution
|
||||||
|
|
||||||
|
The Mosaic framework should improve over time based on usage patterns:
|
||||||
|
|
||||||
|
1. When you discover a recurring pattern that should be codified, capture it to OpenBrain with `type: "framework-improvement"`.
|
||||||
|
2. When a hook, skill, or tool is missing for a common task, capture the gap to OpenBrain with `type: "tooling-gap"`.
|
||||||
|
3. When a framework rule causes friction without adding value, capture the observation to OpenBrain with `type: "framework-friction"`.
|
||||||
|
|
||||||
|
These captures feed the framework's continuous improvement cycle.
|
||||||
|
|
||||||
## Skills Policy
|
## Skills Policy
|
||||||
|
|
||||||
- Use only the minimum required skills for the active task.
|
- Load skills that match the active task domain before starting implementation.
|
||||||
- Do not load unrelated skills.
|
- Do not load unrelated skills.
|
||||||
- Follow skill trigger rules from the active runtime instruction layer.
|
- Follow skill trigger rules from the active runtime instruction layer.
|
||||||
|
- Actively check `~/.config/mosaic/skills/` for applicable skills rather than passively waiting for them to be mentioned.
|
||||||
|
|
||||||
## Session Closure Requirement
|
## Session Closure Requirement
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,20 @@ Universal agent standards layer for Claude Code, Codex, OpenCode, and Pi.
|
|||||||
|
|
||||||
One config, every runtime, same standards.
|
One config, every runtime, same standards.
|
||||||
|
|
||||||
> **This is the framework component of [mosaic-stack](https://git.mosaicstack.dev/mosaic/mosaic-stack).** 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.
|
> **This is the framework component of [mosaic-stack](https://git.mosaicstack.dev/mosaicstack/stack).** 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
|
## Quick Install
|
||||||
|
|
||||||
### Mac / Linux
|
### Mac / Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows (PowerShell)
|
### Windows (PowerShell)
|
||||||
@@ -23,8 +29,8 @@ bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/mai
|
|||||||
### From Source (any platform)
|
### From Source (any platform)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@git.mosaicstack.dev:mosaic/mosaic-stack.git ~/src/mosaic-stack
|
git clone git@git.mosaicstack.dev:mosaicstack/stack.git ~/src/stack
|
||||||
cd ~/src/mosaic-stack && bash tools/install.sh
|
cd ~/src/stack && bash tools/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer:
|
The installer:
|
||||||
@@ -145,13 +151,19 @@ mosaic upgrade check # Check upgrade status (no changes)
|
|||||||
Run the installer again — it handles upgrades automatically:
|
Run the installer again — it handles upgrades automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from a local checkout:
|
Or from a local checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/src/mosaic-stack && git pull && bash tools/install.sh
|
cd ~/src/stack && git pull && bash tools/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer preserves local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/` by default.
|
The installer preserves local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/` by default.
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
||||||
|
|
||||||
# Files preserved across upgrades (never overwritten)
|
# Files/dirs preserved across upgrades (never overwritten).
|
||||||
PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory" "sources")
|
# User-created content in these paths survives rsync --delete.
|
||||||
|
PRESERVE_PATHS=("AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials")
|
||||||
|
|
||||||
# Current framework schema version — bump this when the layout changes.
|
# Current framework schema version — bump this when the layout changes.
|
||||||
# The migration system uses this to run upgrade steps.
|
# The migration system uses this to run upgrade steps.
|
||||||
@@ -217,8 +218,27 @@ fi
|
|||||||
|
|
||||||
sync_framework
|
sync_framework
|
||||||
|
|
||||||
# Ensure memory directory exists
|
# Ensure persistent directories exist
|
||||||
mkdir -p "$TARGET_DIR/memory"
|
mkdir -p "$TARGET_DIR/memory"
|
||||||
|
mkdir -p "$TARGET_DIR/credentials"
|
||||||
|
|
||||||
|
# Seed defaults — copy framework contract files from defaults/ to framework
|
||||||
|
# root if not already present. These ship with sensible defaults but must
|
||||||
|
# never be overwritten once the user has customized them.
|
||||||
|
#
|
||||||
|
# This list must match the framework-contract whitelist in
|
||||||
|
# packages/mosaic/src/config/file-adapter.ts (FileConfigAdapter.syncFramework).
|
||||||
|
# SOUL.md and USER.md are intentionally NOT seeded here — they are generated
|
||||||
|
# by `mosaic init` from templates with user-supplied values.
|
||||||
|
DEFAULTS_DIR="$TARGET_DIR/defaults"
|
||||||
|
if [[ -d "$DEFAULTS_DIR" ]]; then
|
||||||
|
for default_file in AGENTS.md STANDARDS.md TOOLS.md; do
|
||||||
|
if [[ -f "$DEFAULTS_DIR/$default_file" ]] && [[ ! -f "$TARGET_DIR/$default_file" ]]; then
|
||||||
|
cp "$DEFAULTS_DIR/$default_file" "$TARGET_DIR/$default_file"
|
||||||
|
ok "Seeded $default_file from defaults"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Ensure tool scripts are executable
|
# Ensure tool scripts are executable
|
||||||
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
||||||
|
|||||||
@@ -102,3 +102,30 @@ claude mcp add --scope user <name> -- npx -y <package>
|
|||||||
`--scope local` = default, local-only (not committed).
|
`--scope local` = default, local-only (not committed).
|
||||||
|
|
||||||
Do NOT add `mcpServers` to `~/.claude/settings.json` — that key is ignored for MCP loading.
|
Do NOT add `mcpServers` to `~/.claude/settings.json` — that key is ignored for MCP loading.
|
||||||
|
|
||||||
|
## Required Claude Code Settings (Enforced by Launcher)
|
||||||
|
|
||||||
|
The `mosaic claude` launcher validates that `~/.claude/settings.json` contains the required Mosaic configuration. Missing or outdated settings trigger a warning at launch.
|
||||||
|
|
||||||
|
**Required hooks:**
|
||||||
|
|
||||||
|
| Event | Matcher | Script | Purpose |
|
||||||
|
| ----------- | ------------------------ | ------------------------- | ---------------------------------------------- |
|
||||||
|
| PreToolUse | `Write\|Edit\|MultiEdit` | `prevent-memory-write.sh` | Block writes to `~/.claude/projects/*/memory/` |
|
||||||
|
| PostToolUse | `Edit\|MultiEdit\|Write` | `qa-hook-stdin.sh` | QA report generation after code edits |
|
||||||
|
| PostToolUse | `Edit\|MultiEdit\|Write` | `typecheck-hook.sh` | Inline TypeScript type checking |
|
||||||
|
|
||||||
|
**Required plugins:**
|
||||||
|
|
||||||
|
| Plugin | Purpose |
|
||||||
|
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `feature-dev` | Subagent architecture: code-reviewer, code-architect, code-explorer |
|
||||||
|
| `pr-review-toolkit` | PR review: code-simplifier, comment-analyzer, test-analyzer, silent-failure-hunter, type-design-analyzer |
|
||||||
|
| `code-review` | Standalone code review capabilities |
|
||||||
|
|
||||||
|
**Required settings:**
|
||||||
|
|
||||||
|
- `enableAllMcpTools: true` — Allow all configured MCP tools without per-tool approval
|
||||||
|
- `model: "opus"` — Default to opus for orchestrator-level sessions (workers use tiered models via Task tool)
|
||||||
|
|
||||||
|
If `mosaic claude` detects missing hooks or plugins, it will print a warning with the exact settings to add. The session will still launch — enforcement is advisory, not blocking — but agents operating without these settings are running degraded.
|
||||||
|
|||||||
@@ -23,6 +23,16 @@
|
|||||||
"timeout": 60
|
"timeout": 60
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit|MultiEdit|Write",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.config/mosaic/tools/qa/typecheck-hook.sh",
|
||||||
|
"timeout": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,32 +5,32 @@ Project-specific tooling belongs in the project's `AGENTS.md`, not here.
|
|||||||
|
|
||||||
## Mosaic Git Wrappers (Use First)
|
## Mosaic Git Wrappers (Use First)
|
||||||
|
|
||||||
Mosaic wrappers at `~/.config/mosaic/rails/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands.
|
Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Issues
|
# Issues
|
||||||
~/.config/mosaic/rails/git/issue-create.sh
|
~/.config/mosaic/tools/git/issue-create.sh
|
||||||
~/.config/mosaic/rails/git/issue-close.sh
|
~/.config/mosaic/tools/git/issue-close.sh
|
||||||
|
|
||||||
# PRs
|
# PRs
|
||||||
~/.config/mosaic/rails/git/pr-create.sh
|
~/.config/mosaic/tools/git/pr-create.sh
|
||||||
~/.config/mosaic/rails/git/pr-merge.sh
|
~/.config/mosaic/tools/git/pr-merge.sh
|
||||||
|
|
||||||
# Milestones
|
# Milestones
|
||||||
~/.config/mosaic/rails/git/milestone-create.sh
|
~/.config/mosaic/tools/git/milestone-create.sh
|
||||||
|
|
||||||
# CI queue guard (required before push/merge)
|
# CI queue guard (required before push/merge)
|
||||||
~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge
|
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Review (Codex)
|
## Code Review (Codex)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Code quality review
|
# Code quality review
|
||||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted
|
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
|
||||||
|
|
||||||
# Security review
|
# Security review
|
||||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted
|
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
|
||||||
```
|
```
|
||||||
|
|
||||||
## Git Providers
|
## Git Providers
|
||||||
|
|||||||
63
packages/mosaic/framework/tools/qa/typecheck-hook.sh
Executable file
63
packages/mosaic/framework/tools/qa/typecheck-hook.sh
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Lightweight PostToolUse typecheck hook for TypeScript files.
|
||||||
|
# Runs tsc --noEmit on the nearest tsconfig after TS/TSX edits.
|
||||||
|
# Returns non-zero with diagnostic output so the agent sees type errors immediately.
|
||||||
|
# Location: ~/.config/mosaic/tools/qa/typecheck-hook.sh
|
||||||
|
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
# Read JSON from stdin (Claude Code PostToolUse payload)
|
||||||
|
JSON_INPUT=$(cat)
|
||||||
|
|
||||||
|
# Extract file path
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // .tool_response.filePath // .file_path // empty' 2>/dev/null || echo "")
|
||||||
|
else
|
||||||
|
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only check TypeScript files
|
||||||
|
if ! [[ "$FILE_PATH" =~ \.(ts|tsx)$ ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Must be a real file
|
||||||
|
if [ ! -f "$FILE_PATH" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find nearest tsconfig.json by walking up from the file
|
||||||
|
DIR=$(dirname "$FILE_PATH")
|
||||||
|
TSCONFIG=""
|
||||||
|
while [ "$DIR" != "/" ] && [ "$DIR" != "." ]; do
|
||||||
|
if [ -f "$DIR/tsconfig.json" ]; then
|
||||||
|
TSCONFIG="$DIR/tsconfig.json"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
DIR=$(dirname "$DIR")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$TSCONFIG" ]; then
|
||||||
|
# No tsconfig found — skip silently
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tsc --noEmit from the tsconfig directory
|
||||||
|
# Use --pretty for readable output, limit to 10 errors to keep output short
|
||||||
|
TSCONFIG_DIR=$(dirname "$TSCONFIG")
|
||||||
|
cd "$TSCONFIG_DIR"
|
||||||
|
|
||||||
|
# Run typecheck — capture output and exit code
|
||||||
|
OUTPUT=$(npx tsc --noEmit --pretty --maxNodeModuleJsDepth 0 2>&1) || STATUS=$?
|
||||||
|
|
||||||
|
if [ "${STATUS:-0}" -ne 0 ]; then
|
||||||
|
# Filter output to only show errors related to the edited file (if possible)
|
||||||
|
BASENAME=$(basename "$FILE_PATH")
|
||||||
|
RELEVANT=$(echo "$OUTPUT" | grep -A2 "$BASENAME" 2>/dev/null || echo "$OUTPUT" | head -20)
|
||||||
|
|
||||||
|
echo "TypeScript type errors detected after editing $FILE_PATH:"
|
||||||
|
echo "$RELEVANT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.26",
|
"version": "0.0.30",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/mosaic"
|
"directory": "packages/mosaic"
|
||||||
},
|
},
|
||||||
"description": "Mosaic agent framework — installation wizard and meta package",
|
"description": "Mosaic agent framework — installation wizard and meta package",
|
||||||
|
|||||||
@@ -135,15 +135,11 @@ program
|
|||||||
|
|
||||||
// No valid session — prompt for credentials
|
// No valid session — prompt for credentials
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const readline = await import('node:readline');
|
const { promptLine, promptSecret } = await import('./commands/gateway/login.js');
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
||||||
const ask = (q: string): Promise<string> =>
|
|
||||||
new Promise((resolve) => rl.question(q, resolve));
|
|
||||||
|
|
||||||
console.log(`Sign in to ${opts.gateway}`);
|
console.log(`Sign in to ${opts.gateway}`);
|
||||||
const email = await ask('Email: ');
|
const email = await promptLine('Email: ');
|
||||||
const password = await ask('Password: ');
|
const password = await promptSecret('Password: ');
|
||||||
rl.close();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await signIn(opts.gateway, email, password);
|
const auth = await signIn(opts.gateway, email, password);
|
||||||
|
|||||||
111
packages/mosaic/src/commands/launch.spec.ts
Normal file
111
packages/mosaic/src/commands/launch.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import { registerRuntimeLaunchers, type RuntimeLaunchHandler } from './launch.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the commander wiring between `mosaic <runtime>` / `mosaic yolo <runtime>`
|
||||||
|
* subcommands and the internal `launchRuntime` dispatcher.
|
||||||
|
*
|
||||||
|
* Regression target: see mosaicstack/stack#454 — before the fix, `mosaic yolo claude`
|
||||||
|
* passed the literal string "claude" as an excess positional argument to the
|
||||||
|
* underlying CLI, which Claude Code then interpreted as the first user message.
|
||||||
|
*
|
||||||
|
* The bug existed because Commander.js includes declared positional arguments
|
||||||
|
* (here `<runtime>`) in `cmd.args` alongside any true excess args. The action
|
||||||
|
* handler must slice them off before forwarding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function buildProgram(handler: RuntimeLaunchHandler): Command {
|
||||||
|
const program = new Command();
|
||||||
|
program.exitOverride(); // prevent process.exit on parse errors
|
||||||
|
registerRuntimeLaunchers(program, handler);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `process.exit` returns `never`, so vi.spyOn demands a replacement with the
|
||||||
|
// same signature. We throw from the mock to short-circuit into test-land.
|
||||||
|
const exitThrows = (): never => {
|
||||||
|
throw new Error('process.exit called');
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('registerRuntimeLaunchers — non-yolo subcommands', () => {
|
||||||
|
let mockExit: MockInstance<typeof process.exit>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// process.exit is called when the yolo action rejects an invalid runtime.
|
||||||
|
// Stub it so the assertion catches the rejection instead of terminating
|
||||||
|
// the test runner.
|
||||||
|
mockExit = vi.spyOn(process, 'exit').mockImplementation(exitThrows);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockExit.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
|
||||||
|
'forwards %s with empty extraArgs and yolo=false',
|
||||||
|
(runtime) => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const program = buildProgram(handler);
|
||||||
|
program.parse(['node', 'mosaic', runtime]);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handler).toHaveBeenCalledWith(runtime, [], false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('forwards excess args after a non-yolo runtime subcommand', () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const program = buildProgram(handler);
|
||||||
|
program.parse(['node', 'mosaic', 'claude', '--print', 'hello']);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledWith('claude', ['--print', 'hello'], false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registerRuntimeLaunchers — yolo <runtime>', () => {
|
||||||
|
let mockExit: MockInstance<typeof process.exit>;
|
||||||
|
let mockError: MockInstance<typeof console.error>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockExit = vi.spyOn(process, 'exit').mockImplementation(exitThrows);
|
||||||
|
mockError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockExit.mockRestore();
|
||||||
|
mockError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
|
||||||
|
'does NOT pass the runtime name as an extra arg (regression #454) for yolo %s',
|
||||||
|
(runtime) => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const program = buildProgram(handler);
|
||||||
|
program.parse(['node', 'mosaic', 'yolo', runtime]);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
// The critical assertion: extraArgs must be empty, not [runtime].
|
||||||
|
// Before the fix, cmd.args was [runtime] and the runtime name leaked
|
||||||
|
// through to the underlying CLI as an initial positional argument.
|
||||||
|
expect(handler).toHaveBeenCalledWith(runtime, [], true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('forwards true excess args after a yolo runtime', () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const program = buildProgram(handler);
|
||||||
|
program.parse(['node', 'mosaic', 'yolo', 'claude', '--print', 'hi']);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledWith('claude', ['--print', 'hi'], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an unknown runtime under yolo without invoking the handler', () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const program = buildProgram(handler);
|
||||||
|
|
||||||
|
expect(() => program.parse(['node', 'mosaic', 'yolo', 'bogus'])).toThrow('process.exit called');
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -78,6 +78,82 @@ function checkSoul(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Claude settings validation ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface SettingsAudit {
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditClaudeSettings(): SettingsAudit {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
||||||
|
const settings = readJson(settingsPath);
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
warnings.push('~/.claude/settings.json not found — hooks and plugins will be missing');
|
||||||
|
return { warnings };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required hooks
|
||||||
|
const hooks = settings['hooks'] as Record<string, unknown[]> | undefined;
|
||||||
|
|
||||||
|
const requiredPreToolUse = ['prevent-memory-write.sh'];
|
||||||
|
const requiredPostToolUse = ['qa-hook-stdin.sh', 'typecheck-hook.sh'];
|
||||||
|
|
||||||
|
const preHooks = (hooks?.['PreToolUse'] ?? []) as Array<Record<string, unknown>>;
|
||||||
|
const postHooks = (hooks?.['PostToolUse'] ?? []) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
|
const preCommands = preHooks.flatMap((h) => {
|
||||||
|
const inner = (h['hooks'] ?? []) as Array<Record<string, unknown>>;
|
||||||
|
return inner.map((ih) => String(ih['command'] ?? ''));
|
||||||
|
});
|
||||||
|
const postCommands = postHooks.flatMap((h) => {
|
||||||
|
const inner = (h['hooks'] ?? []) as Array<Record<string, unknown>>;
|
||||||
|
return inner.map((ih) => String(ih['command'] ?? ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const script of requiredPreToolUse) {
|
||||||
|
if (!preCommands.some((c) => c.includes(script))) {
|
||||||
|
warnings.push(`Missing PreToolUse hook: ${script}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const script of requiredPostToolUse) {
|
||||||
|
if (!postCommands.some((c) => c.includes(script))) {
|
||||||
|
warnings.push(`Missing PostToolUse hook: ${script}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required plugins
|
||||||
|
const plugins = (settings['enabledPlugins'] ?? {}) as Record<string, boolean>;
|
||||||
|
const requiredPlugins = ['feature-dev', 'pr-review-toolkit', 'code-review'];
|
||||||
|
|
||||||
|
for (const plugin of requiredPlugins) {
|
||||||
|
const found = Object.keys(plugins).some((k) => k.startsWith(plugin) && plugins[k]);
|
||||||
|
if (!found) {
|
||||||
|
warnings.push(`Missing plugin: ${plugin}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check enableAllMcpTools
|
||||||
|
if (!settings['enableAllMcpTools']) {
|
||||||
|
warnings.push('enableAllMcpTools is not true — MCP tools may require per-tool approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { warnings };
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSettingsWarnings(audit: SettingsAudit): void {
|
||||||
|
if (audit.warnings.length === 0) return;
|
||||||
|
|
||||||
|
console.log('\n[mosaic] Claude Code settings audit:');
|
||||||
|
for (const w of audit.warnings) {
|
||||||
|
console.log(` ⚠ ${w}`);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
'[mosaic] Run: mosaic doctor — or see ~/.config/mosaic/runtime/claude/RUNTIME.md for required settings.\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function checkSequentialThinking(runtime: string): void {
|
function checkSequentialThinking(runtime: string): void {
|
||||||
const checker = fwScript('mosaic-ensure-sequential-thinking');
|
const checker = fwScript('mosaic-ensure-sequential-thinking');
|
||||||
if (!existsSync(checker)) return; // Skip if checker doesn't exist
|
if (!existsSync(checker)) return; // Skip if checker doesn't exist
|
||||||
@@ -407,6 +483,10 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
|||||||
|
|
||||||
switch (runtime) {
|
switch (runtime) {
|
||||||
case 'claude': {
|
case 'claude': {
|
||||||
|
// Audit Claude Code settings and warn about missing hooks/plugins
|
||||||
|
const settingsAudit = auditClaudeSettings();
|
||||||
|
printSettingsWarnings(settingsAudit);
|
||||||
|
|
||||||
const prompt = buildRuntimePrompt('claude');
|
const prompt = buildRuntimePrompt('claude');
|
||||||
const cliArgs = yolo ? ['--dangerously-skip-permissions'] : [];
|
const cliArgs = yolo ? ['--dangerously-skip-permissions'] : [];
|
||||||
cliArgs.push('--append-system-prompt', prompt);
|
cliArgs.push('--append-system-prompt', prompt);
|
||||||
@@ -677,8 +757,23 @@ function runUpgrade(args: string[]): never {
|
|||||||
|
|
||||||
// ─── Commander registration ─────────────────────────────────────────────────
|
// ─── Commander registration ─────────────────────────────────────────────────
|
||||||
|
|
||||||
export function registerLaunchCommands(program: Command): void {
|
/**
|
||||||
// Runtime launchers
|
* Handler invoked when a runtime subcommand (`<runtime>` or `yolo <runtime>`)
|
||||||
|
* is parsed. Exposed so tests can exercise the commander wiring without
|
||||||
|
* spawning subprocesses.
|
||||||
|
*/
|
||||||
|
export type RuntimeLaunchHandler = (
|
||||||
|
runtime: RuntimeName,
|
||||||
|
extraArgs: string[],
|
||||||
|
yolo: boolean,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire `<runtime>` and `yolo <runtime>` subcommands onto `program` using a
|
||||||
|
* pluggable launch handler. Separated from `registerLaunchCommands` so tests
|
||||||
|
* can inject a spy and verify argument forwarding.
|
||||||
|
*/
|
||||||
|
export function registerRuntimeLaunchers(program: Command, handler: RuntimeLaunchHandler): void {
|
||||||
for (const runtime of ['claude', 'codex', 'opencode', 'pi'] as const) {
|
for (const runtime of ['claude', 'codex', 'opencode', 'pi'] as const) {
|
||||||
program
|
program
|
||||||
.command(runtime)
|
.command(runtime)
|
||||||
@@ -686,11 +781,10 @@ export function registerLaunchCommands(program: Command): void {
|
|||||||
.allowUnknownOption(true)
|
.allowUnknownOption(true)
|
||||||
.allowExcessArguments(true)
|
.allowExcessArguments(true)
|
||||||
.action((_opts: unknown, cmd: Command) => {
|
.action((_opts: unknown, cmd: Command) => {
|
||||||
launchRuntime(runtime, cmd.args, false);
|
handler(runtime, cmd.args, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yolo mode
|
|
||||||
program
|
program
|
||||||
.command('yolo <runtime>')
|
.command('yolo <runtime>')
|
||||||
.description('Launch a runtime in dangerous-permissions mode (claude|codex|opencode|pi)')
|
.description('Launch a runtime in dangerous-permissions mode (claude|codex|opencode|pi)')
|
||||||
@@ -704,8 +798,21 @@ export function registerLaunchCommands(program: Command): void {
|
|||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
launchRuntime(runtime as RuntimeName, cmd.args, true);
|
// Commander includes declared positional arguments (`<runtime>`) in
|
||||||
|
// `cmd.args` alongside any trailing excess args. Slice off the first
|
||||||
|
// element so we forward only true excess args — otherwise the runtime
|
||||||
|
// name leaks into the underlying CLI as an initial positional arg,
|
||||||
|
// which Claude Code interprets as the first user message.
|
||||||
|
// Regression test: launch.spec.ts, issue mosaicstack/stack#454.
|
||||||
|
handler(runtime as RuntimeName, cmd.args.slice(1), true);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerLaunchCommands(program: Command): void {
|
||||||
|
// Runtime launchers + yolo mode wired to the real process-replacing launcher.
|
||||||
|
registerRuntimeLaunchers(program, (runtime, extraArgs, yolo) => {
|
||||||
|
launchRuntime(runtime, extraArgs, yolo);
|
||||||
|
});
|
||||||
|
|
||||||
// Coord (mission orchestrator)
|
// Coord (mission orchestrator)
|
||||||
program
|
program
|
||||||
|
|||||||
134
packages/mosaic/src/config/file-adapter.test.ts
Normal file
134
packages/mosaic/src/config/file-adapter.test.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { FileConfigAdapter, DEFAULT_SEED_FILES } from './file-adapter.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression tests for the `FileConfigAdapter.syncFramework` seed behavior.
|
||||||
|
*
|
||||||
|
* Background: the bash installer (`framework/install.sh`) and this TS wizard
|
||||||
|
* path both seed framework-contract files from `framework/defaults/` into the
|
||||||
|
* user's mosaic home on first install. Before this fix:
|
||||||
|
*
|
||||||
|
* - The bash installer only seeded `AGENTS.md` and `STANDARDS.md`, leaving
|
||||||
|
* `TOOLS.md` missing despite it being listed as mandatory in the
|
||||||
|
* AGENTS.md load order (position 5).
|
||||||
|
* - The TS wizard iterated every file in `defaults/` and copied it to the
|
||||||
|
* mosaic home root — including `defaults/SOUL.md` (hardcoded "Jarvis"),
|
||||||
|
* `defaults/USER.md` (placeholder), and internal framework files like
|
||||||
|
* `README.md` and `AUDIT-*.md`. That clobbered the identity flow on
|
||||||
|
* fresh installs and leaked framework-internal clutter into the user's
|
||||||
|
* home directory.
|
||||||
|
*
|
||||||
|
* This suite pins the whitelist and the preservation semantics so both
|
||||||
|
* regressions stay fixed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function makeFixture(): { sourceDir: string; mosaicHome: string; defaultsDir: string } {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'mosaic-file-adapter-'));
|
||||||
|
const sourceDir = join(root, 'source');
|
||||||
|
const mosaicHome = join(root, 'mosaic-home');
|
||||||
|
const defaultsDir = join(sourceDir, 'defaults');
|
||||||
|
|
||||||
|
mkdirSync(defaultsDir, { recursive: true });
|
||||||
|
mkdirSync(mosaicHome, { recursive: true });
|
||||||
|
|
||||||
|
// Framework-contract defaults we expect the wizard to seed.
|
||||||
|
writeFileSync(join(defaultsDir, 'AGENTS.md'), '# AGENTS default\n');
|
||||||
|
writeFileSync(join(defaultsDir, 'STANDARDS.md'), '# STANDARDS default\n');
|
||||||
|
writeFileSync(join(defaultsDir, 'TOOLS.md'), '# TOOLS default\n');
|
||||||
|
|
||||||
|
// Non-contract files we must NOT seed on first install.
|
||||||
|
writeFileSync(join(defaultsDir, 'SOUL.md'), '# SOUL default (should not be seeded)\n');
|
||||||
|
writeFileSync(join(defaultsDir, 'USER.md'), '# USER default (should not be seeded)\n');
|
||||||
|
writeFileSync(join(defaultsDir, 'README.md'), '# README (framework-internal)\n');
|
||||||
|
writeFileSync(
|
||||||
|
join(defaultsDir, 'AUDIT-2026-02-17-framework-consistency.md'),
|
||||||
|
'# Audit snapshot\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
return { sourceDir, mosaicHome, defaultsDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FileConfigAdapter.syncFramework — defaults seeding', () => {
|
||||||
|
let fixture: ReturnType<typeof makeFixture>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(join(fixture.sourceDir, '..'), { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('seeds the three framework-contract files on a fresh mosaic home', async () => {
|
||||||
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
||||||
|
|
||||||
|
await adapter.syncFramework('fresh');
|
||||||
|
|
||||||
|
for (const name of DEFAULT_SEED_FILES) {
|
||||||
|
expect(existsSync(join(fixture.mosaicHome, name))).toBe(true);
|
||||||
|
}
|
||||||
|
expect(readFileSync(join(fixture.mosaicHome, 'TOOLS.md'), 'utf-8')).toContain(
|
||||||
|
'# TOOLS default',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT seed SOUL.md or USER.md from defaults/ (wizard stages own those)', async () => {
|
||||||
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
||||||
|
|
||||||
|
await adapter.syncFramework('fresh');
|
||||||
|
|
||||||
|
// SOUL.md and USER.md live in defaults/ for historical reasons, but they
|
||||||
|
// are template-rendered per-user by the wizard stages. Seeding them here
|
||||||
|
// would clobber the identity flow and leak placeholder content.
|
||||||
|
expect(existsSync(join(fixture.mosaicHome, 'SOUL.md'))).toBe(false);
|
||||||
|
expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT seed README.md or AUDIT-*.md from defaults/', async () => {
|
||||||
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
||||||
|
|
||||||
|
await adapter.syncFramework('fresh');
|
||||||
|
|
||||||
|
expect(existsSync(join(fixture.mosaicHome, 'README.md'))).toBe(false);
|
||||||
|
expect(existsSync(join(fixture.mosaicHome, 'AUDIT-2026-02-17-framework-consistency.md'))).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing contract files — never overwrites user customization', async () => {
|
||||||
|
// Also plant a root-level AGENTS.md in sourceDir so that `syncDirectory`
|
||||||
|
// itself (not just the seed loop) has something to try to overwrite.
|
||||||
|
// Without this, the test would silently pass even if preserve semantics
|
||||||
|
// were broken in syncDirectory.
|
||||||
|
writeFileSync(join(fixture.sourceDir, 'AGENTS.md'), '# shipped AGENTS from source root\n');
|
||||||
|
|
||||||
|
writeFileSync(join(fixture.mosaicHome, 'TOOLS.md'), '# user-customized TOOLS\n');
|
||||||
|
writeFileSync(join(fixture.mosaicHome, 'AGENTS.md'), '# user-customized AGENTS\n');
|
||||||
|
|
||||||
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
||||||
|
await adapter.syncFramework('keep');
|
||||||
|
|
||||||
|
expect(readFileSync(join(fixture.mosaicHome, 'TOOLS.md'), 'utf-8')).toBe(
|
||||||
|
'# user-customized TOOLS\n',
|
||||||
|
);
|
||||||
|
expect(readFileSync(join(fixture.mosaicHome, 'AGENTS.md'), 'utf-8')).toBe(
|
||||||
|
'# user-customized AGENTS\n',
|
||||||
|
);
|
||||||
|
// And the missing contract file still gets seeded.
|
||||||
|
expect(readFileSync(join(fixture.mosaicHome, 'STANDARDS.md'), 'utf-8')).toContain(
|
||||||
|
'# STANDARDS default',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a no-op for seeding when defaults/ dir does not exist', async () => {
|
||||||
|
rmSync(fixture.defaultsDir, { recursive: true });
|
||||||
|
|
||||||
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
||||||
|
await expect(adapter.syncFramework('fresh')).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(existsSync(join(fixture.mosaicHome, 'TOOLS.md'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
import { readFileSync, existsSync, readdirSync, statSync, copyFileSync } from 'node:fs';
|
import { readFileSync, existsSync, statSync, copyFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Framework-contract files that `syncFramework` seeds from `framework/defaults/`
|
||||||
|
* into the mosaic home root on first install. These are the only files the
|
||||||
|
* wizard is allowed to touch as a one-time seed — SOUL.md and USER.md are
|
||||||
|
* generated from templates by their respective wizard stages with
|
||||||
|
* user-supplied values, and anything else under `defaults/` (README.md,
|
||||||
|
* audit snapshots, etc.) is framework-internal and must not leak into the
|
||||||
|
* user's mosaic home.
|
||||||
|
*
|
||||||
|
* This list must match the explicit seed loop in
|
||||||
|
* packages/mosaic/framework/install.sh.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_SEED_FILES = ['AGENTS.md', 'STANDARDS.md', 'TOOLS.md'] as const;
|
||||||
import type { ConfigService, ConfigSection, ResolvedConfig } from './config-service.js';
|
import type { ConfigService, ConfigSection, ResolvedConfig } from './config-service.js';
|
||||||
import type { SoulConfig, UserConfig, ToolsConfig, InstallAction } from '../types.js';
|
import type { SoulConfig, UserConfig, ToolsConfig, InstallAction } from '../types.js';
|
||||||
import { soulSchema, userSchema, toolsSchema } from './schemas.js';
|
import { soulSchema, userSchema, toolsSchema } from './schemas.js';
|
||||||
@@ -131,9 +145,24 @@ export class FileConfigAdapter implements ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncFramework(action: InstallAction): Promise<void> {
|
async syncFramework(action: InstallAction): Promise<void> {
|
||||||
|
// Must match PRESERVE_PATHS in packages/mosaic/framework/install.sh so
|
||||||
|
// the bash and TS install paths have the same upgrade-preservation
|
||||||
|
// semantics. Contract files (AGENTS.md, STANDARDS.md, TOOLS.md) are
|
||||||
|
// seeded from defaults/ on first install and preserved thereafter;
|
||||||
|
// identity files (SOUL.md, USER.md) are generated by wizard stages and
|
||||||
|
// must never be touched by the framework sync.
|
||||||
const preservePaths =
|
const preservePaths =
|
||||||
action === 'keep' || action === 'reconfigure'
|
action === 'keep' || action === 'reconfigure'
|
||||||
? ['SOUL.md', 'USER.md', 'TOOLS.md', 'memory']
|
? [
|
||||||
|
'AGENTS.md',
|
||||||
|
'SOUL.md',
|
||||||
|
'USER.md',
|
||||||
|
'TOOLS.md',
|
||||||
|
'STANDARDS.md',
|
||||||
|
'memory',
|
||||||
|
'sources',
|
||||||
|
'credentials',
|
||||||
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
syncDirectory(this.sourceDir, this.mosaicHome, {
|
syncDirectory(this.sourceDir, this.mosaicHome, {
|
||||||
@@ -141,20 +170,23 @@ export class FileConfigAdapter implements ConfigService {
|
|||||||
excludeGit: true,
|
excludeGit: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy default root-level .md files (AGENTS.md, STANDARDS.md, etc.)
|
// Copy framework-contract files (AGENTS.md, STANDARDS.md, TOOLS.md)
|
||||||
// from framework/defaults/ into mosaicHome root if they don't exist yet.
|
// from framework/defaults/ into the mosaic home root if they don't
|
||||||
// These are framework contracts — only written on first install, never
|
// exist yet. These are written on first install only and are never
|
||||||
// overwritten (user may have customized them).
|
// overwritten afterwards — the user may have customized them.
|
||||||
|
//
|
||||||
|
// SOUL.md and USER.md are deliberately NOT seeded here. They are
|
||||||
|
// generated from templates by the soul/user wizard stages with
|
||||||
|
// user-supplied values; seeding them from defaults would clobber the
|
||||||
|
// identity flow and leak placeholder content into the mosaic home.
|
||||||
const defaultsDir = join(this.sourceDir, 'defaults');
|
const defaultsDir = join(this.sourceDir, 'defaults');
|
||||||
if (existsSync(defaultsDir)) {
|
if (existsSync(defaultsDir)) {
|
||||||
for (const entry of readdirSync(defaultsDir)) {
|
for (const entry of DEFAULT_SEED_FILES) {
|
||||||
|
const src = join(defaultsDir, entry);
|
||||||
const dest = join(this.mosaicHome, entry);
|
const dest = join(this.mosaicHome, entry);
|
||||||
if (!existsSync(dest)) {
|
if (existsSync(dest)) continue;
|
||||||
const src = join(defaultsDir, entry);
|
if (!existsSync(src) || !statSync(src).isFile()) continue;
|
||||||
if (statSync(src).isFile()) {
|
copyFileSync(src, dest);
|
||||||
copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,53 @@ export const DEFAULTS = {
|
|||||||
| (add your git providers here) | | | |`,
|
| (add your git providers here) | | | |`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Preset intent categories with display labels and suggested agent names. */
|
||||||
|
export const INTENT_PRESETS: Record<
|
||||||
|
string,
|
||||||
|
{ label: string; hint: string; suggestedName: string }
|
||||||
|
> = {
|
||||||
|
general: {
|
||||||
|
label: 'General purpose assistant',
|
||||||
|
hint: 'Versatile helper for any task',
|
||||||
|
suggestedName: 'Mosaic',
|
||||||
|
},
|
||||||
|
'software-dev': {
|
||||||
|
label: 'Software development',
|
||||||
|
hint: 'Coding, debugging, architecture',
|
||||||
|
suggestedName: 'Forge',
|
||||||
|
},
|
||||||
|
devops: {
|
||||||
|
label: 'DevOps & infrastructure',
|
||||||
|
hint: 'CI/CD, containers, monitoring',
|
||||||
|
suggestedName: 'Sentinel',
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
label: 'Research & analysis',
|
||||||
|
hint: 'Data analysis, literature review',
|
||||||
|
suggestedName: 'Atlas',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
label: 'Content & writing',
|
||||||
|
hint: 'Documentation, copywriting, editing',
|
||||||
|
suggestedName: 'Muse',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
label: 'Custom',
|
||||||
|
hint: 'Describe your own use case',
|
||||||
|
suggestedName: 'Mosaic',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect LLM provider type from an API key prefix.
|
||||||
|
*/
|
||||||
|
export function detectProviderType(key: string): 'anthropic' | 'openai' | 'none' {
|
||||||
|
if (!key) return 'none';
|
||||||
|
if (key.startsWith('sk-ant-')) return 'anthropic';
|
||||||
|
if (key.startsWith('sk-')) return 'openai';
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
export const RECOMMENDED_SKILLS = new Set([
|
export const RECOMMENDED_SKILLS = new Set([
|
||||||
'brainstorming',
|
'brainstorming',
|
||||||
'code-review-excellence',
|
'code-review-excellence',
|
||||||
|
|||||||
129
packages/mosaic/src/stages/agent-intent.spec.ts
Normal file
129
packages/mosaic/src/stages/agent-intent.spec.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import type { WizardState } from '../types.js';
|
||||||
|
import { agentIntentStage } from './agent-intent.js';
|
||||||
|
|
||||||
|
function buildPrompter(overrides: Partial<Record<string, unknown>> = {}) {
|
||||||
|
return {
|
||||||
|
intro: vi.fn(),
|
||||||
|
outro: vi.fn(),
|
||||||
|
note: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
text: vi.fn().mockResolvedValue('Mosaic'),
|
||||||
|
confirm: vi.fn().mockResolvedValue(false),
|
||||||
|
select: vi.fn().mockResolvedValue('general'),
|
||||||
|
multiselect: vi.fn(),
|
||||||
|
groupMultiselect: vi.fn(),
|
||||||
|
spinner: vi.fn().mockReturnValue({ update: vi.fn(), stop: vi.fn() }),
|
||||||
|
separator: vi.fn(),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeState(): WizardState {
|
||||||
|
return {
|
||||||
|
mosaicHome: '/tmp/mosaic',
|
||||||
|
sourceDir: '/tmp/mosaic',
|
||||||
|
mode: 'quick',
|
||||||
|
installAction: 'fresh',
|
||||||
|
soul: {},
|
||||||
|
user: {},
|
||||||
|
tools: {},
|
||||||
|
runtimes: { detected: [], mcpConfigured: false },
|
||||||
|
selectedSkills: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('agentIntentStage', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default intent and name in headless mode', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
delete process.env['MOSAIC_AGENT_INTENT'];
|
||||||
|
delete process.env['MOSAIC_AGENT_NAME'];
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await agentIntentStage(p, state);
|
||||||
|
|
||||||
|
expect(state.agentIntent).toBe('general');
|
||||||
|
expect(state.soul.agentName).toBe('Mosaic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads intent from MOSAIC_AGENT_INTENT env var', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
process.env['MOSAIC_AGENT_INTENT'] = 'software-dev';
|
||||||
|
delete process.env['MOSAIC_AGENT_NAME'];
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await agentIntentStage(p, state);
|
||||||
|
|
||||||
|
expect(state.agentIntent).toBe('software-dev');
|
||||||
|
expect(state.soul.agentName).toBe('Forge');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors MOSAIC_AGENT_NAME env var override', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
process.env['MOSAIC_AGENT_INTENT'] = 'devops';
|
||||||
|
process.env['MOSAIC_AGENT_NAME'] = 'MyBot';
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await agentIntentStage(p, state);
|
||||||
|
|
||||||
|
expect(state.agentIntent).toBe('devops');
|
||||||
|
expect(state.soul.agentName).toBe('MyBot');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to general for unknown intent values', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
process.env['MOSAIC_AGENT_INTENT'] = 'nonexistent';
|
||||||
|
delete process.env['MOSAIC_AGENT_NAME'];
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await agentIntentStage(p, state);
|
||||||
|
|
||||||
|
expect(state.agentIntent).toBe('general');
|
||||||
|
expect(state.soul.agentName).toBe('Mosaic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prompts for intent and name in interactive mode', async () => {
|
||||||
|
delete process.env['MOSAIC_ASSUME_YES'];
|
||||||
|
const origIsTTY = process.stdin.isTTY;
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
||||||
|
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter({
|
||||||
|
select: vi.fn().mockResolvedValue('research'),
|
||||||
|
text: vi.fn().mockResolvedValue('Atlas'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentIntentStage(p, state);
|
||||||
|
|
||||||
|
expect(state.agentIntent).toBe('research');
|
||||||
|
expect(state.soul.agentName).toBe('Atlas');
|
||||||
|
expect(p.select).toHaveBeenCalled();
|
||||||
|
expect(p.text).toHaveBeenCalled();
|
||||||
|
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps content intent to Muse suggested name', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
process.env['MOSAIC_AGENT_INTENT'] = 'content';
|
||||||
|
delete process.env['MOSAIC_AGENT_NAME'];
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await agentIntentStage(p, state);
|
||||||
|
|
||||||
|
expect(state.agentIntent).toBe('content');
|
||||||
|
expect(state.soul.agentName).toBe('Muse');
|
||||||
|
});
|
||||||
|
});
|
||||||
64
packages/mosaic/src/stages/agent-intent.ts
Normal file
64
packages/mosaic/src/stages/agent-intent.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { WizardPrompter } from '../prompter/interface.js';
|
||||||
|
import type { AgentIntent, WizardState } from '../types.js';
|
||||||
|
import { INTENT_PRESETS } from '../constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent intent + naming stage — deterministic (no LLM required).
|
||||||
|
*
|
||||||
|
* The user picks an intent category from presets, the system proposes a
|
||||||
|
* thematic name, and the user confirms or overrides it.
|
||||||
|
*
|
||||||
|
* In headless mode, reads from `MOSAIC_AGENT_INTENT` and `MOSAIC_AGENT_NAME`.
|
||||||
|
*/
|
||||||
|
export async function agentIntentStage(p: WizardPrompter, state: WizardState): Promise<void> {
|
||||||
|
const isHeadless = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
||||||
|
|
||||||
|
if (isHeadless) {
|
||||||
|
const intentEnv = process.env['MOSAIC_AGENT_INTENT'] ?? 'general';
|
||||||
|
const nameEnv = process.env['MOSAIC_AGENT_NAME'];
|
||||||
|
const preset = INTENT_PRESETS[intentEnv] ?? INTENT_PRESETS['general']!;
|
||||||
|
state.agentIntent ??= (intentEnv in INTENT_PRESETS ? intentEnv : 'general') as AgentIntent;
|
||||||
|
// Respect existing agentName (e.g. from CLI overrides) — only set from
|
||||||
|
// env/preset if not already populated.
|
||||||
|
state.soul.agentName ??= nameEnv ?? preset.suggestedName;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.separator();
|
||||||
|
p.note(
|
||||||
|
'Tell us what this agent will primarily help you with.\n' +
|
||||||
|
"We'll suggest a name based on your choice — you can always change it.",
|
||||||
|
'Agent Identity',
|
||||||
|
);
|
||||||
|
|
||||||
|
const intentOptions = Object.entries(INTENT_PRESETS).map(([value, info]) => ({
|
||||||
|
value: value as AgentIntent,
|
||||||
|
label: info.label,
|
||||||
|
hint: info.hint,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const intent = await p.select<AgentIntent>({
|
||||||
|
message: 'What will this agent primarily help you with?',
|
||||||
|
options: intentOptions,
|
||||||
|
initialValue: 'general' as AgentIntent,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.agentIntent = intent;
|
||||||
|
|
||||||
|
const preset = INTENT_PRESETS[intent];
|
||||||
|
const suggestedName = preset?.suggestedName ?? 'Mosaic';
|
||||||
|
|
||||||
|
const name = await p.text({
|
||||||
|
message: `Your agent will be named "${suggestedName}". Press Enter to accept or type a new name`,
|
||||||
|
initialValue: suggestedName,
|
||||||
|
defaultValue: suggestedName,
|
||||||
|
validate: (v) => {
|
||||||
|
if (v.length === 0) return 'Name cannot be empty';
|
||||||
|
if (v.length > 50) return 'Name must be under 50 characters';
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
state.soul.agentName = name;
|
||||||
|
p.log(`Agent name set to: ${name}`);
|
||||||
|
}
|
||||||
@@ -126,6 +126,14 @@ export interface GatewayConfigStageOptions {
|
|||||||
portOverride?: number;
|
portOverride?: number;
|
||||||
/** Skip the `npm install -g @mosaicstack/gateway` step (local build / tests). */
|
/** Skip the `npm install -g @mosaicstack/gateway` step (local build / tests). */
|
||||||
skipInstall?: boolean;
|
skipInstall?: boolean;
|
||||||
|
/**
|
||||||
|
* Pre-collected provider API key (from the provider-setup stage or Quick
|
||||||
|
* Start path). When set, the gateway-config stage will skip the interactive
|
||||||
|
* API key prompt and use this value directly.
|
||||||
|
*/
|
||||||
|
providerKey?: string;
|
||||||
|
/** Provider type detected from the key prefix. */
|
||||||
|
providerType?: 'anthropic' | 'openai' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayConfigStageResult {
|
export interface GatewayConfigStageResult {
|
||||||
@@ -314,6 +322,8 @@ export async function gatewayConfigStage(
|
|||||||
envFile: ENV_FILE,
|
envFile: ENV_FILE,
|
||||||
mosaicConfigFile: MOSAIC_CONFIG_FILE,
|
mosaicConfigFile: MOSAIC_CONFIG_FILE,
|
||||||
gatewayHome: GATEWAY_HOME,
|
gatewayHome: GATEWAY_HOME,
|
||||||
|
providerKey: opts.providerKey,
|
||||||
|
providerType: opts.providerType,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof GatewayConfigValidationError) {
|
if (err instanceof GatewayConfigValidationError) {
|
||||||
@@ -389,6 +399,10 @@ interface CollectOptions {
|
|||||||
envFile: string;
|
envFile: string;
|
||||||
mosaicConfigFile: string;
|
mosaicConfigFile: string;
|
||||||
gatewayHome: string;
|
gatewayHome: string;
|
||||||
|
/** Pre-collected API key — skips the interactive prompt when set. */
|
||||||
|
providerKey?: string;
|
||||||
|
/** Provider type — determines the env var name for the key. */
|
||||||
|
providerType?: 'anthropic' | 'openai' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raised by the config stage when headless env validation fails. */
|
/** Raised by the config stage when headless env validation fails. */
|
||||||
@@ -466,10 +480,15 @@ async function collectAndWriteConfig(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
anthropicKey = await p.text({
|
if (opts.providerKey) {
|
||||||
message: 'ANTHROPIC_API_KEY (optional, press Enter to skip)',
|
anthropicKey = opts.providerKey;
|
||||||
defaultValue: '',
|
p.log(`Using API key from provider setup (${opts.providerType ?? 'unknown'}).`);
|
||||||
});
|
} else {
|
||||||
|
anthropicKey = await p.text({
|
||||||
|
message: 'ANTHROPIC_API_KEY (optional, press Enter to skip)',
|
||||||
|
defaultValue: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hostname = await p.text({
|
hostname = await p.text({
|
||||||
message: 'Web UI hostname (for browser access)',
|
message: 'Web UI hostname (for browser access)',
|
||||||
@@ -508,7 +527,11 @@ async function collectAndWriteConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (anthropicKey) {
|
if (anthropicKey) {
|
||||||
envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
|
if (opts.providerType === 'openai') {
|
||||||
|
envLines.push(`OPENAI_API_KEY=${anthropicKey}`);
|
||||||
|
} else {
|
||||||
|
envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(opts.envFile, envLines.join('\n') + '\n', { mode: 0o600 });
|
writeFileSync(opts.envFile, envLines.join('\n') + '\n', { mode: 0o600 });
|
||||||
|
|||||||
118
packages/mosaic/src/stages/provider-setup.spec.ts
Normal file
118
packages/mosaic/src/stages/provider-setup.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import type { WizardState } from '../types.js';
|
||||||
|
import { providerSetupStage } from './provider-setup.js';
|
||||||
|
|
||||||
|
function buildPrompter(overrides: Partial<Record<string, unknown>> = {}) {
|
||||||
|
return {
|
||||||
|
intro: vi.fn(),
|
||||||
|
outro: vi.fn(),
|
||||||
|
note: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
confirm: vi.fn().mockResolvedValue(false),
|
||||||
|
select: vi.fn().mockResolvedValue('general'),
|
||||||
|
multiselect: vi.fn(),
|
||||||
|
groupMultiselect: vi.fn(),
|
||||||
|
spinner: vi.fn().mockReturnValue({ update: vi.fn(), stop: vi.fn() }),
|
||||||
|
separator: vi.fn(),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeState(): WizardState {
|
||||||
|
return {
|
||||||
|
mosaicHome: '/tmp/mosaic',
|
||||||
|
sourceDir: '/tmp/mosaic',
|
||||||
|
mode: 'quick',
|
||||||
|
installAction: 'fresh',
|
||||||
|
soul: {},
|
||||||
|
user: {},
|
||||||
|
tools: {},
|
||||||
|
runtimes: { detected: [], mcpConfigured: false },
|
||||||
|
selectedSkills: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('providerSetupStage', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Anthropic key from prefix in headless mode', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
process.env['MOSAIC_ANTHROPIC_API_KEY'] = 'sk-ant-api03-test123';
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await providerSetupStage(p, state);
|
||||||
|
|
||||||
|
expect(state.providerKey).toBe('sk-ant-api03-test123');
|
||||||
|
expect(state.providerType).toBe('anthropic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects OpenAI key from prefix in headless mode', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
process.env['MOSAIC_OPENAI_API_KEY'] = 'sk-proj-test123';
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await providerSetupStage(p, state);
|
||||||
|
|
||||||
|
expect(state.providerKey).toBe('sk-proj-test123');
|
||||||
|
expect(state.providerType).toBe('openai');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets provider type to none when no key is provided in headless mode', async () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
delete process.env['MOSAIC_ANTHROPIC_API_KEY'];
|
||||||
|
delete process.env['MOSAIC_OPENAI_API_KEY'];
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter();
|
||||||
|
|
||||||
|
await providerSetupStage(p, state);
|
||||||
|
|
||||||
|
expect(state.providerKey).toBeUndefined();
|
||||||
|
expect(state.providerType).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prompts for key in interactive mode', async () => {
|
||||||
|
delete process.env['MOSAIC_ASSUME_YES'];
|
||||||
|
// Simulate a TTY
|
||||||
|
const origIsTTY = process.stdin.isTTY;
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
||||||
|
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter({
|
||||||
|
text: vi.fn().mockResolvedValue('sk-ant-api03-interactive'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await providerSetupStage(p, state);
|
||||||
|
|
||||||
|
expect(p.text).toHaveBeenCalled();
|
||||||
|
expect(state.providerKey).toBe('sk-ant-api03-interactive');
|
||||||
|
expect(state.providerType).toBe('anthropic');
|
||||||
|
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty key in interactive mode', async () => {
|
||||||
|
delete process.env['MOSAIC_ASSUME_YES'];
|
||||||
|
const origIsTTY = process.stdin.isTTY;
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
||||||
|
|
||||||
|
const state = makeState();
|
||||||
|
const p = buildPrompter({
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
await providerSetupStage(p, state);
|
||||||
|
|
||||||
|
expect(state.providerType).toBe('none');
|
||||||
|
expect(state.providerKey).toBeUndefined();
|
||||||
|
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
54
packages/mosaic/src/stages/provider-setup.ts
Normal file
54
packages/mosaic/src/stages/provider-setup.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { WizardPrompter } from '../prompter/interface.js';
|
||||||
|
import type { WizardState } from '../types.js';
|
||||||
|
import { detectProviderType } from '../constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider setup stage — collects the user's LLM API key and detects the
|
||||||
|
* provider type from the key prefix.
|
||||||
|
*
|
||||||
|
* In headless mode, reads from `MOSAIC_ANTHROPIC_API_KEY` or `MOSAIC_OPENAI_API_KEY`.
|
||||||
|
*/
|
||||||
|
export async function providerSetupStage(p: WizardPrompter, state: WizardState): Promise<void> {
|
||||||
|
const isHeadless = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
||||||
|
|
||||||
|
if (isHeadless) {
|
||||||
|
const anthropicKey = process.env['MOSAIC_ANTHROPIC_API_KEY'] ?? '';
|
||||||
|
const openaiKey = process.env['MOSAIC_OPENAI_API_KEY'] ?? '';
|
||||||
|
const key = anthropicKey || openaiKey;
|
||||||
|
state.providerKey = key || undefined;
|
||||||
|
state.providerType = detectProviderType(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.separator();
|
||||||
|
p.note(
|
||||||
|
'Configure your LLM provider so the agent has a brain.\n' +
|
||||||
|
'Anthropic (Claude) and OpenAI are supported.\n' +
|
||||||
|
'You can skip this and add a key later via `mosaic configure`.',
|
||||||
|
'LLM Provider',
|
||||||
|
);
|
||||||
|
|
||||||
|
const key = await p.text({
|
||||||
|
message: 'API key (paste your Anthropic or OpenAI key, or press Enter to skip)',
|
||||||
|
defaultValue: '',
|
||||||
|
placeholder: 'sk-ant-api03-... or sk-...',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
const provider = detectProviderType(key);
|
||||||
|
state.providerKey = key;
|
||||||
|
state.providerType = provider;
|
||||||
|
|
||||||
|
if (provider === 'anthropic') {
|
||||||
|
p.log('Detected provider: Anthropic (Claude)');
|
||||||
|
} else if (provider === 'openai') {
|
||||||
|
p.log('Detected provider: OpenAI');
|
||||||
|
} else {
|
||||||
|
p.log('Provider auto-detection failed. Key will be stored as ANTHROPIC_API_KEY.');
|
||||||
|
state.providerType = 'anthropic';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.providerType = 'none';
|
||||||
|
p.log('No API key provided. You can add one later with `mosaic configure`.');
|
||||||
|
}
|
||||||
|
}
|
||||||
98
packages/mosaic/src/stages/quick-start.ts
Normal file
98
packages/mosaic/src/stages/quick-start.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { WizardPrompter } from '../prompter/interface.js';
|
||||||
|
import type { ConfigService } from '../config/config-service.js';
|
||||||
|
import type { WizardState } from '../types.js';
|
||||||
|
import { DEFAULTS } from '../constants.js';
|
||||||
|
import { providerSetupStage } from './provider-setup.js';
|
||||||
|
import { runtimeSetupStage } from './runtime-setup.js';
|
||||||
|
import { hooksPreviewStage } from './hooks-preview.js';
|
||||||
|
import { skillsSelectStage } from './skills-select.js';
|
||||||
|
import { finalizeStage } from './finalize.js';
|
||||||
|
import { gatewayConfigStage } from './gateway-config.js';
|
||||||
|
import { gatewayBootstrapStage } from './gateway-bootstrap.js';
|
||||||
|
|
||||||
|
export interface QuickStartOptions {
|
||||||
|
skipGateway?: boolean;
|
||||||
|
gatewayHost?: string;
|
||||||
|
gatewayPort?: number;
|
||||||
|
gatewayPortOverride?: number;
|
||||||
|
skipGatewayNpmInstall?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick Start path — minimal questions to get a working agent.
|
||||||
|
*
|
||||||
|
* 1. Provider API key
|
||||||
|
* 2. Admin email + password (via gateway bootstrap)
|
||||||
|
* 3. Everything else uses defaults.
|
||||||
|
*
|
||||||
|
* Target: under 90 seconds for a returning user.
|
||||||
|
*/
|
||||||
|
export async function quickStartPath(
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
state: WizardState,
|
||||||
|
configService: ConfigService,
|
||||||
|
options: QuickStartOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
state.mode = 'quick';
|
||||||
|
|
||||||
|
// 1. Provider setup (first question)
|
||||||
|
await providerSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// Apply sensible defaults for everything else
|
||||||
|
state.soul.agentName ??= 'Mosaic';
|
||||||
|
state.soul.roleDescription ??= DEFAULTS.roleDescription;
|
||||||
|
state.soul.communicationStyle ??= 'direct';
|
||||||
|
state.user.background = DEFAULTS.background;
|
||||||
|
state.user.accessibilitySection = DEFAULTS.accessibilitySection;
|
||||||
|
state.user.personalBoundaries = DEFAULTS.personalBoundaries;
|
||||||
|
state.tools.gitProviders = [];
|
||||||
|
state.tools.credentialsLocation = DEFAULTS.credentialsLocation;
|
||||||
|
state.tools.customToolsSection = DEFAULTS.customToolsSection;
|
||||||
|
|
||||||
|
// Runtime detection (auto, no user input in quick mode)
|
||||||
|
await runtimeSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// Hooks (auto-accept in quick mode for Claude)
|
||||||
|
await hooksPreviewStage(prompter, state);
|
||||||
|
|
||||||
|
// Skills (recommended set, no user input in quick mode)
|
||||||
|
await skillsSelectStage(prompter, state);
|
||||||
|
|
||||||
|
// Finalize (writes configs, links runtime assets, syncs skills)
|
||||||
|
await finalizeStage(prompter, state, configService);
|
||||||
|
|
||||||
|
// Gateway config + bootstrap
|
||||||
|
if (!options.skipGateway) {
|
||||||
|
const headlessRun = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configResult = await gatewayConfigStage(prompter, state, {
|
||||||
|
host: options.gatewayHost ?? 'localhost',
|
||||||
|
defaultPort: options.gatewayPort ?? 14242,
|
||||||
|
portOverride: options.gatewayPortOverride,
|
||||||
|
skipInstall: options.skipGatewayNpmInstall,
|
||||||
|
providerKey: state.providerKey,
|
||||||
|
providerType: state.providerType ?? 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configResult.ready || !configResult.host || !configResult.port) {
|
||||||
|
if (headlessRun) {
|
||||||
|
prompter.warn('Gateway configuration failed in headless mode — aborting wizard.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
|
||||||
|
host: configResult.host,
|
||||||
|
port: configResult.port,
|
||||||
|
});
|
||||||
|
if (!bootstrapResult.completed) {
|
||||||
|
prompter.warn('Admin bootstrap failed — aborting wizard.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
packages/mosaic/src/stages/wizard-menu.spec.ts
Normal file
118
packages/mosaic/src/stages/wizard-menu.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import type { MenuSection } from '../types.js';
|
||||||
|
import { detectProviderType, INTENT_PRESETS } from '../constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the drill-down menu system and its supporting utilities.
|
||||||
|
*
|
||||||
|
* The menu loop itself is in wizard.ts and is hard to unit test in isolation
|
||||||
|
* because it orchestrates many async stages. These tests verify the building
|
||||||
|
* blocks: provider detection, intent presets, and the WizardState shape.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('detectProviderType', () => {
|
||||||
|
it('detects Anthropic from sk-ant- prefix', () => {
|
||||||
|
expect(detectProviderType('sk-ant-api03-abc123')).toBe('anthropic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects OpenAI from sk- prefix', () => {
|
||||||
|
expect(detectProviderType('sk-proj-abc123')).toBe('openai');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns none for empty string', () => {
|
||||||
|
expect(detectProviderType('')).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns none for unrecognized prefix', () => {
|
||||||
|
expect(detectProviderType('gsk_abc123')).toBe('none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('INTENT_PRESETS', () => {
|
||||||
|
it('has all expected intent categories', () => {
|
||||||
|
expect(Object.keys(INTENT_PRESETS)).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
'general',
|
||||||
|
'software-dev',
|
||||||
|
'devops',
|
||||||
|
'research',
|
||||||
|
'content',
|
||||||
|
'custom',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each preset has label, hint, and suggestedName', () => {
|
||||||
|
for (const [key, preset] of Object.entries(INTENT_PRESETS)) {
|
||||||
|
expect(preset.label, `${key}.label`).toBeTruthy();
|
||||||
|
expect(preset.hint, `${key}.hint`).toBeTruthy();
|
||||||
|
expect(preset.suggestedName, `${key}.suggestedName`).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps software-dev to Forge', () => {
|
||||||
|
expect(INTENT_PRESETS['software-dev']?.suggestedName).toBe('Forge');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps devops to Sentinel', () => {
|
||||||
|
expect(INTENT_PRESETS['devops']?.suggestedName).toBe('Sentinel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WizardState completedSections', () => {
|
||||||
|
it('tracks completed sections as a Set', () => {
|
||||||
|
const completed = new Set<MenuSection>();
|
||||||
|
completed.add('providers');
|
||||||
|
completed.add('identity');
|
||||||
|
|
||||||
|
expect(completed.has('providers')).toBe(true);
|
||||||
|
expect(completed.has('identity')).toBe(true);
|
||||||
|
expect(completed.has('skills')).toBe(false);
|
||||||
|
expect(completed.size).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('headless backward compat', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MOSAIC_ASSUME_YES=1 triggers headless path', () => {
|
||||||
|
process.env['MOSAIC_ASSUME_YES'] = '1';
|
||||||
|
const isHeadless = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
||||||
|
expect(isHeadless).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-TTY triggers headless path', () => {
|
||||||
|
delete process.env['MOSAIC_ASSUME_YES'];
|
||||||
|
// In test environments, process.stdin.isTTY is typically undefined (falsy)
|
||||||
|
const isHeadless = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
||||||
|
expect(isHeadless).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all headless env vars are recognized', () => {
|
||||||
|
// This test documents the expected env vars for headless installs.
|
||||||
|
const headlessVars = [
|
||||||
|
'MOSAIC_ASSUME_YES',
|
||||||
|
'MOSAIC_ADMIN_NAME',
|
||||||
|
'MOSAIC_ADMIN_EMAIL',
|
||||||
|
'MOSAIC_ADMIN_PASSWORD',
|
||||||
|
'MOSAIC_GATEWAY_PORT',
|
||||||
|
'MOSAIC_HOSTNAME',
|
||||||
|
'MOSAIC_CORS_ORIGIN',
|
||||||
|
'MOSAIC_STORAGE_TIER',
|
||||||
|
'MOSAIC_DATABASE_URL',
|
||||||
|
'MOSAIC_VALKEY_URL',
|
||||||
|
'MOSAIC_ANTHROPIC_API_KEY',
|
||||||
|
'MOSAIC_AGENT_NAME',
|
||||||
|
'MOSAIC_AGENT_INTENT',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Just verify none of them throw when accessed
|
||||||
|
for (const v of headlessVars) {
|
||||||
|
expect(() => process.env[v]).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,19 @@ export type InstallAction = 'fresh' | 'keep' | 'reconfigure' | 'reset';
|
|||||||
export type CommunicationStyle = 'direct' | 'friendly' | 'formal';
|
export type CommunicationStyle = 'direct' | 'friendly' | 'formal';
|
||||||
export type RuntimeName = 'claude' | 'codex' | 'opencode' | 'pi';
|
export type RuntimeName = 'claude' | 'codex' | 'opencode' | 'pi';
|
||||||
|
|
||||||
|
export type MenuSection =
|
||||||
|
| 'quick-start'
|
||||||
|
| 'providers'
|
||||||
|
| 'identity'
|
||||||
|
| 'skills'
|
||||||
|
| 'gateway'
|
||||||
|
| 'advanced'
|
||||||
|
| 'finish';
|
||||||
|
|
||||||
|
export type AgentIntent = 'general' | 'software-dev' | 'devops' | 'research' | 'content' | 'custom';
|
||||||
|
|
||||||
|
export type ProviderType = 'anthropic' | 'openai' | 'none';
|
||||||
|
|
||||||
export interface SoulConfig {
|
export interface SoulConfig {
|
||||||
agentName?: string;
|
agentName?: string;
|
||||||
roleDescription?: string;
|
roleDescription?: string;
|
||||||
@@ -86,4 +99,12 @@ export interface WizardState {
|
|||||||
selectedSkills: string[];
|
selectedSkills: string[];
|
||||||
hooks?: HooksState;
|
hooks?: HooksState;
|
||||||
gateway?: GatewayState;
|
gateway?: GatewayState;
|
||||||
|
/** Tracks which menu sections have been completed in drill-down mode. */
|
||||||
|
completedSections?: Set<MenuSection>;
|
||||||
|
/** The user's chosen agent intent category. */
|
||||||
|
agentIntent?: AgentIntent;
|
||||||
|
/** The LLM provider API key entered during setup. */
|
||||||
|
providerKey?: string;
|
||||||
|
/** Detected provider type based on API key prefix. */
|
||||||
|
providerType?: ProviderType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { WizardPrompter } from './prompter/interface.js';
|
import type { WizardPrompter } from './prompter/interface.js';
|
||||||
import type { ConfigService } from './config/config-service.js';
|
import type { ConfigService } from './config/config-service.js';
|
||||||
import type { WizardState } from './types.js';
|
import type { MenuSection, WizardState } from './types.js';
|
||||||
import { welcomeStage } from './stages/welcome.js';
|
import { welcomeStage } from './stages/welcome.js';
|
||||||
import { detectInstallStage } from './stages/detect-install.js';
|
import { detectInstallStage } from './stages/detect-install.js';
|
||||||
import { modeSelectStage } from './stages/mode-select.js';
|
|
||||||
import { soulSetupStage } from './stages/soul-setup.js';
|
import { soulSetupStage } from './stages/soul-setup.js';
|
||||||
import { userSetupStage } from './stages/user-setup.js';
|
import { userSetupStage } from './stages/user-setup.js';
|
||||||
import { toolsSetupStage } from './stages/tools-setup.js';
|
import { toolsSetupStage } from './stages/tools-setup.js';
|
||||||
@@ -13,6 +12,10 @@ import { skillsSelectStage } from './stages/skills-select.js';
|
|||||||
import { finalizeStage } from './stages/finalize.js';
|
import { finalizeStage } from './stages/finalize.js';
|
||||||
import { gatewayConfigStage } from './stages/gateway-config.js';
|
import { gatewayConfigStage } from './stages/gateway-config.js';
|
||||||
import { gatewayBootstrapStage } from './stages/gateway-bootstrap.js';
|
import { gatewayBootstrapStage } from './stages/gateway-bootstrap.js';
|
||||||
|
import { providerSetupStage } from './stages/provider-setup.js';
|
||||||
|
import { agentIntentStage } from './stages/agent-intent.js';
|
||||||
|
import { quickStartPath } from './stages/quick-start.js';
|
||||||
|
import { DEFAULTS } from './constants.js';
|
||||||
|
|
||||||
export interface WizardOptions {
|
export interface WizardOptions {
|
||||||
mosaicHome: string;
|
mosaicHome: string;
|
||||||
@@ -54,6 +57,7 @@ export async function runWizard(options: WizardOptions): Promise<void> {
|
|||||||
tools: {},
|
tools: {},
|
||||||
runtimes: { detected: [], mcpConfigured: false },
|
runtimes: { detected: [], mcpConfigured: false },
|
||||||
selectedSkills: [],
|
selectedSkills: [],
|
||||||
|
completedSections: new Set<MenuSection>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply CLI overrides (strip undefined values)
|
// Apply CLI overrides (strip undefined values)
|
||||||
@@ -90,55 +94,304 @@ export async function runWizard(options: WizardOptions): Promise<void> {
|
|||||||
// Stage 2: Existing Install Detection
|
// Stage 2: Existing Install Detection
|
||||||
await detectInstallStage(prompter, state, configService);
|
await detectInstallStage(prompter, state, configService);
|
||||||
|
|
||||||
// Stage 3: Quick Start vs Advanced (skip if keeping existing)
|
// ── Headless bypass ────────────────────────────────────────────────────────
|
||||||
if (state.installAction === 'fresh' || state.installAction === 'reset') {
|
// When MOSAIC_ASSUME_YES=1 or no TTY, run the linear headless path.
|
||||||
await modeSelectStage(prompter, state);
|
// This preserves full backward compatibility with tools/install.sh --yes.
|
||||||
} else if (state.installAction === 'reconfigure') {
|
const headlessRun = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
||||||
state.mode = 'advanced';
|
if (headlessRun) {
|
||||||
|
await runHeadlessPath(prompter, state, configService, options);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 4: SOUL.md
|
// ── Interactive: Main Menu ─────────────────────────────────────────────────
|
||||||
|
if (state.installAction === 'fresh' || state.installAction === 'reset') {
|
||||||
|
await runMenuLoop(prompter, state, configService, options);
|
||||||
|
} else if (state.installAction === 'reconfigure') {
|
||||||
|
state.mode = 'advanced';
|
||||||
|
await runMenuLoop(prompter, state, configService, options);
|
||||||
|
} else {
|
||||||
|
// 'keep' — skip identity setup, go straight to finalize + gateway
|
||||||
|
await runKeepPath(prompter, state, configService, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Menu-driven interactive flow ────────────────────────────────────────────
|
||||||
|
|
||||||
|
type MenuChoice =
|
||||||
|
| 'quick-start'
|
||||||
|
| 'providers'
|
||||||
|
| 'identity'
|
||||||
|
| 'skills'
|
||||||
|
| 'gateway-config'
|
||||||
|
| 'advanced'
|
||||||
|
| 'finish';
|
||||||
|
|
||||||
|
function menuLabel(section: MenuChoice, completed: Set<MenuSection>): string {
|
||||||
|
const labels: Record<MenuChoice, string> = {
|
||||||
|
'quick-start': 'Quick Start',
|
||||||
|
providers: 'Providers',
|
||||||
|
identity: 'Agent Identity',
|
||||||
|
skills: 'Skills',
|
||||||
|
'gateway-config': 'Gateway',
|
||||||
|
advanced: 'Advanced',
|
||||||
|
finish: 'Finish & Apply',
|
||||||
|
};
|
||||||
|
const base = labels[section];
|
||||||
|
const sectionKey: MenuSection =
|
||||||
|
section === 'gateway-config' ? 'gateway' : (section as MenuSection);
|
||||||
|
if (completed.has(sectionKey)) {
|
||||||
|
return `${base} [done]`;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMenuLoop(
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
state: WizardState,
|
||||||
|
configService: ConfigService,
|
||||||
|
options: WizardOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const completed = state.completedSections!;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
const choice = await prompter.select<MenuChoice>({
|
||||||
|
message: 'What would you like to configure?',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'quick-start',
|
||||||
|
label: menuLabel('quick-start', completed),
|
||||||
|
hint: 'Recommended defaults, minimal questions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'providers',
|
||||||
|
label: menuLabel('providers', completed),
|
||||||
|
hint: 'LLM API keys (Anthropic, OpenAI)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'identity',
|
||||||
|
label: menuLabel('identity', completed),
|
||||||
|
hint: 'Agent name, intent, persona',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'skills',
|
||||||
|
label: menuLabel('skills', completed),
|
||||||
|
hint: 'Install agent skills',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'gateway-config',
|
||||||
|
label: menuLabel('gateway-config', completed),
|
||||||
|
hint: 'Port, storage, database',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'advanced',
|
||||||
|
label: menuLabel('advanced', completed),
|
||||||
|
hint: 'SOUL.md, USER.md, TOOLS.md, runtimes, hooks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'finish',
|
||||||
|
label: menuLabel('finish', completed),
|
||||||
|
hint: 'Write configs and start gateway',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (choice) {
|
||||||
|
case 'quick-start':
|
||||||
|
await quickStartPath(prompter, state, configService, options);
|
||||||
|
return; // Quick start is a complete flow — exit menu
|
||||||
|
|
||||||
|
case 'providers':
|
||||||
|
await providerSetupStage(prompter, state);
|
||||||
|
completed.add('providers');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'identity':
|
||||||
|
await agentIntentStage(prompter, state);
|
||||||
|
completed.add('identity');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'skills':
|
||||||
|
await skillsSelectStage(prompter, state);
|
||||||
|
completed.add('skills');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gateway-config':
|
||||||
|
// Gateway config is handled during Finish — mark as "configured"
|
||||||
|
// after user reviews settings.
|
||||||
|
await runGatewaySubMenu(prompter, state, options);
|
||||||
|
completed.add('gateway');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'advanced':
|
||||||
|
await runAdvancedSubMenu(prompter, state);
|
||||||
|
completed.add('advanced');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'finish':
|
||||||
|
await runFinishPath(prompter, state, configService, options);
|
||||||
|
return; // Done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gateway sub-menu ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runGatewaySubMenu(
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
state: WizardState,
|
||||||
|
_options: WizardOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
prompter.note(
|
||||||
|
'Gateway settings will be applied when you select "Finish & Apply".\n' +
|
||||||
|
'Configure the settings you want to customize here.',
|
||||||
|
'Gateway Configuration',
|
||||||
|
);
|
||||||
|
|
||||||
|
// For now, just let them know defaults will be used and they can
|
||||||
|
// override during finish. The actual gateway config stage runs
|
||||||
|
// during Finish & Apply. This menu item exists so users know
|
||||||
|
// the gateway is part of the wizard.
|
||||||
|
const port = await prompter.text({
|
||||||
|
message: 'Gateway port',
|
||||||
|
initialValue: (_options.gatewayPort ?? 14242).toString(),
|
||||||
|
defaultValue: (_options.gatewayPort ?? 14242).toString(),
|
||||||
|
validate: (v) => {
|
||||||
|
const n = parseInt(v, 10);
|
||||||
|
if (Number.isNaN(n) || n < 1 || n > 65535) return 'Port must be 1-65535';
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store for later use in the gateway config stage
|
||||||
|
_options.gatewayPort = parseInt(port, 10);
|
||||||
|
prompter.log(`Gateway port set to ${port}. Will be applied during Finish & Apply.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Advanced sub-menu ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runAdvancedSubMenu(prompter: WizardPrompter, state: WizardState): Promise<void> {
|
||||||
|
state.mode = 'advanced';
|
||||||
|
|
||||||
|
// Run the detailed setup stages
|
||||||
await soulSetupStage(prompter, state);
|
await soulSetupStage(prompter, state);
|
||||||
|
|
||||||
// Stage 5: USER.md
|
|
||||||
await userSetupStage(prompter, state);
|
await userSetupStage(prompter, state);
|
||||||
|
|
||||||
// Stage 6: TOOLS.md
|
|
||||||
await toolsSetupStage(prompter, state);
|
await toolsSetupStage(prompter, state);
|
||||||
|
|
||||||
// Stage 7: Runtime Detection & Installation
|
|
||||||
await runtimeSetupStage(prompter, state);
|
await runtimeSetupStage(prompter, state);
|
||||||
|
|
||||||
// Stage 8: Hooks preview (Claude only — skipped if Claude not detected)
|
|
||||||
await hooksPreviewStage(prompter, state);
|
await hooksPreviewStage(prompter, state);
|
||||||
|
}
|
||||||
|
|
||||||
// Stage 9: Skills Selection
|
// ── Finish & Apply ──────────────────────────────────────────────────────────
|
||||||
await skillsSelectStage(prompter, state);
|
|
||||||
|
|
||||||
// Stage 10: Finalize (writes configs, links runtime assets, runs doctor)
|
async function runFinishPath(
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
state: WizardState,
|
||||||
|
configService: ConfigService,
|
||||||
|
options: WizardOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
// Apply defaults for anything not explicitly configured
|
||||||
|
state.soul.agentName ??= 'Mosaic';
|
||||||
|
state.soul.roleDescription ??= DEFAULTS.roleDescription;
|
||||||
|
state.soul.communicationStyle ??= 'direct';
|
||||||
|
state.user.background ??= DEFAULTS.background;
|
||||||
|
state.user.accessibilitySection ??= DEFAULTS.accessibilitySection;
|
||||||
|
state.user.personalBoundaries ??= DEFAULTS.personalBoundaries;
|
||||||
|
state.tools.gitProviders ??= [];
|
||||||
|
state.tools.credentialsLocation ??= DEFAULTS.credentialsLocation;
|
||||||
|
state.tools.customToolsSection ??= DEFAULTS.customToolsSection;
|
||||||
|
|
||||||
|
// Runtime detection if not already done
|
||||||
|
if (state.runtimes.detected.length === 0 && !state.completedSections?.has('advanced')) {
|
||||||
|
await runtimeSetupStage(prompter, state);
|
||||||
|
await hooksPreviewStage(prompter, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills defaults if not already configured
|
||||||
|
if (!state.completedSections?.has('skills')) {
|
||||||
|
await skillsSelectStage(prompter, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize (writes configs, links runtime assets, syncs skills)
|
||||||
await finalizeStage(prompter, state, configService);
|
await finalizeStage(prompter, state, configService);
|
||||||
|
|
||||||
// Stages 11 & 12: Gateway config + admin bootstrap.
|
// Gateway stages
|
||||||
// The unified first-run flow runs these as terminal stages so the user
|
|
||||||
// goes from "welcome" through "admin user created" in a single cohesive
|
|
||||||
// experience. Callers that only want the framework portion pass
|
|
||||||
// `skipGateway: true`.
|
|
||||||
if (!options.skipGateway) {
|
if (!options.skipGateway) {
|
||||||
const headlessRun = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configResult = await gatewayConfigStage(prompter, state, {
|
const configResult = await gatewayConfigStage(prompter, state, {
|
||||||
host: options.gatewayHost ?? 'localhost',
|
host: options.gatewayHost ?? 'localhost',
|
||||||
defaultPort: options.gatewayPort ?? 14242,
|
defaultPort: options.gatewayPort ?? 14242,
|
||||||
portOverride: options.gatewayPortOverride,
|
portOverride: options.gatewayPortOverride,
|
||||||
skipInstall: options.skipGatewayNpmInstall,
|
skipInstall: options.skipGatewayNpmInstall,
|
||||||
|
providerKey: state.providerKey,
|
||||||
|
providerType: state.providerType ?? 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (configResult.ready && configResult.host && configResult.port) {
|
||||||
|
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
|
||||||
|
host: configResult.host,
|
||||||
|
port: configResult.port,
|
||||||
|
});
|
||||||
|
if (!bootstrapResult.completed) {
|
||||||
|
prompter.warn('Admin bootstrap failed — aborting wizard.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Headless linear path (backward compat) ──────────────────────────────────
|
||||||
|
|
||||||
|
async function runHeadlessPath(
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
state: WizardState,
|
||||||
|
configService: ConfigService,
|
||||||
|
options: WizardOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
// Provider setup from env vars
|
||||||
|
await providerSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// Agent intent from env vars
|
||||||
|
await agentIntentStage(prompter, state);
|
||||||
|
|
||||||
|
// SOUL.md
|
||||||
|
await soulSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// USER.md
|
||||||
|
await userSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// TOOLS.md
|
||||||
|
await toolsSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// Runtime Detection
|
||||||
|
await runtimeSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
await hooksPreviewStage(prompter, state);
|
||||||
|
|
||||||
|
// Skills
|
||||||
|
await skillsSelectStage(prompter, state);
|
||||||
|
|
||||||
|
// Finalize
|
||||||
|
await finalizeStage(prompter, state, configService);
|
||||||
|
|
||||||
|
// Gateway stages
|
||||||
|
if (!options.skipGateway) {
|
||||||
|
try {
|
||||||
|
const configResult = await gatewayConfigStage(prompter, state, {
|
||||||
|
host: options.gatewayHost ?? 'localhost',
|
||||||
|
defaultPort: options.gatewayPort ?? 14242,
|
||||||
|
portOverride: options.gatewayPortOverride,
|
||||||
|
skipInstall: options.skipGatewayNpmInstall,
|
||||||
|
providerKey: state.providerKey,
|
||||||
|
providerType: state.providerType ?? 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!configResult.ready || !configResult.host || !configResult.port) {
|
if (!configResult.ready || !configResult.host || !configResult.port) {
|
||||||
if (headlessRun) {
|
prompter.warn('Gateway configuration failed in headless mode — aborting wizard.');
|
||||||
prompter.warn('Gateway configuration failed in headless mode — aborting wizard.');
|
process.exit(1);
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
|
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
|
||||||
host: configResult.host,
|
host: configResult.host,
|
||||||
@@ -150,12 +403,53 @@ export async function runWizard(options: WizardOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Stages normally return structured `ready: false` results for
|
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
// expected failures. Anything that reaches here is an unexpected
|
throw err;
|
||||||
// runtime error — render a concise warning for UX AND re-throw so
|
}
|
||||||
// the CLI (and `tools/install.sh` auto-launch) sees a non-zero exit.
|
}
|
||||||
// Swallowing here would let headless installs report success even
|
}
|
||||||
// when the gateway stage crashed.
|
|
||||||
|
// ── Keep path (preserve existing identity) ──────────────────────────────────
|
||||||
|
|
||||||
|
async function runKeepPath(
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
state: WizardState,
|
||||||
|
configService: ConfigService,
|
||||||
|
options: WizardOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
// Runtime detection
|
||||||
|
await runtimeSetupStage(prompter, state);
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
await hooksPreviewStage(prompter, state);
|
||||||
|
|
||||||
|
// Skills
|
||||||
|
await skillsSelectStage(prompter, state);
|
||||||
|
|
||||||
|
// Finalize
|
||||||
|
await finalizeStage(prompter, state, configService);
|
||||||
|
|
||||||
|
// Gateway stages
|
||||||
|
if (!options.skipGateway) {
|
||||||
|
try {
|
||||||
|
const configResult = await gatewayConfigStage(prompter, state, {
|
||||||
|
host: options.gatewayHost ?? 'localhost',
|
||||||
|
defaultPort: options.gatewayPort ?? 14242,
|
||||||
|
portOverride: options.gatewayPortOverride,
|
||||||
|
skipInstall: options.skipGatewayNpmInstall,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (configResult.ready && configResult.host && configResult.port) {
|
||||||
|
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
|
||||||
|
host: configResult.host,
|
||||||
|
port: configResult.port,
|
||||||
|
});
|
||||||
|
if (!bootstrapResult.completed) {
|
||||||
|
prompter.warn('Admin bootstrap failed — aborting wizard.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/prdy"
|
"directory": "packages/prdy"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/quality-rails"
|
"directory": "packages/quality-rails"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/queue"
|
"directory": "packages/queue"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/storage"
|
"directory": "packages/storage"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "packages/types"
|
"directory": "packages/types"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "plugins/discord"
|
"directory": "plugins/discord"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "plugins/macp"
|
"directory": "plugins/macp"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "plugins/mosaic-framework"
|
"directory": "plugins/mosaic-framework"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
"directory": "plugins/telegram"
|
"directory": "plugins/telegram"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
# 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools)
|
# 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools)
|
||||||
# 2. @mosaicstack/mosaic (npm) → ~/.npm-global/ (CLI, TUI, gateway client, wizard)
|
# 2. @mosaicstack/mosaic (npm) → ~/.npm-global/ (CLI, TUI, gateway client, wizard)
|
||||||
#
|
#
|
||||||
# Remote install (recommended):
|
# Quick: curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
# bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
|
# Direct: bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
#
|
#
|
||||||
# Remote install (alternative — use -s -- to pass flags):
|
# Remote install (alternative — use -s -- to pass flags):
|
||||||
# curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh | bash -s --
|
# curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh | bash -s --
|
||||||
#
|
#
|
||||||
# Flags:
|
# Flags:
|
||||||
# --check Version check only, no install
|
# --check Version check only, no install
|
||||||
@@ -69,7 +69,7 @@ REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstac
|
|||||||
SCOPE="${MOSAIC_SCOPE:-@mosaicstack}"
|
SCOPE="${MOSAIC_SCOPE:-@mosaicstack}"
|
||||||
PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}"
|
PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}"
|
||||||
CLI_PKG="${SCOPE}/mosaic"
|
CLI_PKG="${SCOPE}/mosaic"
|
||||||
REPO_BASE="https://git.mosaicstack.dev/mosaicstack/mosaic-stack"
|
REPO_BASE="https://git.mosaicstack.dev/mosaicstack/stack"
|
||||||
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
|
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
|
||||||
|
|
||||||
# ─── uninstall path ───────────────────────────────────────────────────────────
|
# ─── uninstall path ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user