Compare commits

..

1 Commits

Author SHA1 Message Date
Jarvis
d6117f1a4a docs: scaffold install-ux-v2 mission + archive install-ux-hardening
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Parent mission install-ux-hardening-20260405 shipped at mosaic-v0.0.25
but a real-run test uncovered a critical bootstrap regression plus a
cluster of first-run UX failings. New mission install-ux-v2-20260405
closes the regression as a hotfix (M01 → mosaic-v0.0.26), polishes
CORS/skill installer UX (M02), then rethinks the flow around a
drill-down main menu with a provider-first intent intake and real
quick-start (M03 → mosaic-v0.0.27).

Linked issues: #436 (M01), #437 (M02), #438 (M03)
Archives install-ux-hardening-20260405 under docs/archive/missions/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 16:23:51 -05:00
65 changed files with 160 additions and 2716 deletions

View File

@@ -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/stack/gateway:sha-${CI_COMMIT_SHA:0:7}" DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-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/stack/gateway:latest" DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:latest"
fi fi
if [ -n "$CI_COMMIT_TAG" ]; then if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:$CI_COMMIT_TAG" DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-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/stack/web:sha-${CI_COMMIT_SHA:0:7}" DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-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/stack/web:latest" DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:latest"
fi fi
if [ -n "$CI_COMMIT_TAG" ]; then if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG" DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-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:

View File

@@ -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.1` | Cost-sensitive coding where Codex is unavailable | Z.ai credits | | `glm-5` | 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.

View File

@@ -7,13 +7,7 @@ Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi —
## Quick Install ## Quick Install
```bash ```bash
curl -fsSL https://mosaicstack.dev/install.sh | bash bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
```
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:
@@ -185,8 +179,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/stack.git git clone git@git.mosaicstack.dev:mosaicstack/mosaic-stack.git
cd stack cd mosaic-stack
# Start infrastructure (Postgres, Valkey, Jaeger) # Start infrastructure (Postgres, Valkey, Jaeger)
docker compose up -d docker compose up -d
@@ -235,7 +229,7 @@ npm packages are published to the Gitea package registry on main merges.
## Architecture ## Architecture
``` ```
stack/ mosaic-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)
@@ -308,13 +302,7 @@ 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
curl -fsSL https://mosaicstack.dev/install.sh | bash bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
```
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:

View File

@@ -3,7 +3,7 @@
"version": "0.0.6", "version": "0.0.6",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "apps/gateway" "directory": "apps/gateway"
}, },
"type": "module", "type": "module",
@@ -72,17 +72,11 @@
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/testing": "^11.1.18",
"@swc/core": "^1.15.24",
"@swc/helpers": "^0.5.21",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/supertest": "^7.2.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"supertest": "^7.2.2",
"tsx": "^4.0.0", "tsx": "^4.0.0",
"typescript": "^5.8.0", "typescript": "^5.8.0",
"unplugin-swc": "^1.5.9",
"vitest": "^2.0.0" "vitest": "^2.0.0"
} }
} }

View File

@@ -13,8 +13,7 @@ import type { Auth } from '@mosaicstack/auth';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { AUTH } from '../auth/auth.tokens.js'; import { AUTH } from '../auth/auth.tokens.js';
import { DB } from '../database/database.module.js'; import { DB } from '../database/database.module.js';
import { BootstrapSetupDto } from './bootstrap.dto.js'; import type { BootstrapSetupDto, BootstrapStatusDto, BootstrapResultDto } from './bootstrap.dto.js';
import type { BootstrapStatusDto, BootstrapResultDto } from './bootstrap.dto.js';
@Controller('api/bootstrap') @Controller('api/bootstrap')
export class BootstrapController { export class BootstrapController {

View File

@@ -1,190 +0,0 @@
/**
* E2E integration test — POST /api/bootstrap/setup
*
* Regression guard for the `import type { BootstrapSetupDto }` class-erasure
* bug (IUV-M01, issue #436).
*
* When `BootstrapSetupDto` is imported with `import type`, TypeScript erases
* the class at compile time. NestJS then sees `Object` as the `@Body()`
* metatype, and ValidationPipe with `whitelist:true + forbidNonWhitelisted:true`
* treats every property as non-whitelisted, returning:
*
* 400 { message: ["property email should not exist", "property password should not exist"] }
*
* The fix is a plain value import (`import { BootstrapSetupDto }`), which
* preserves the class reference so Nest can read the class-validator decorators.
*
* This test MUST fail if `import type` is re-introduced on `BootstrapSetupDto`.
* A controller unit test that constructs ValidationPipe manually won't catch
* this — only the real DI binding path exercises the metatype lookup.
*/
import 'reflect-metadata';
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import { Test } from '@nestjs/testing';
import { ValidationPipe, type INestApplication } from '@nestjs/common';
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
import request from 'supertest';
import { BootstrapController } from './bootstrap.controller.js';
import type { BootstrapResultDto } from './bootstrap.dto.js';
// ─── Minimal mock dependencies ───────────────────────────────────────────────
/**
* We use explicit `@Inject(AUTH)` / `@Inject(DB)` in the controller so we
* can provide mock values by token without spinning up the real DB or Auth.
*/
import { AUTH } from '../auth/auth.tokens.js';
import { DB } from '../database/database.module.js';
const MOCK_USER_ID = 'mock-user-id-001';
const mockAuth = {
api: {
createUser: () =>
Promise.resolve({
user: {
id: MOCK_USER_ID,
name: 'Admin',
email: 'admin@example.com',
},
}),
},
};
// Override db.select() so the second query (verify user exists) returns a user.
// The bootstrap controller calls select().from() twice:
// 1. count() to check zero users → returns [{total: 0}]
// 2. select().where().limit() → returns [the created user]
let selectCallCount = 0;
const mockDbWithUser = {
select: () => {
selectCallCount++;
return {
from: () => {
if (selectCallCount === 1) {
// First call: count — zero users
return Promise.resolve([{ total: 0 }]);
}
// Subsequent calls: return a mock user row
return {
where: () => ({
limit: () =>
Promise.resolve([
{
id: MOCK_USER_ID,
name: 'Admin',
email: 'admin@example.com',
role: 'admin',
},
]),
}),
};
},
};
},
update: () => ({
set: () => ({
where: () => Promise.resolve([]),
}),
}),
insert: () => ({
values: () => ({
returning: () =>
Promise.resolve([
{
id: 'token-id-001',
label: 'Initial setup token',
},
]),
}),
}),
};
// ─── Test suite ───────────────────────────────────────────────────────────────
describe('POST /api/bootstrap/setup — ValidationPipe DTO binding', () => {
let app: INestApplication;
beforeAll(async () => {
selectCallCount = 0;
const moduleRef = await Test.createTestingModule({
controllers: [BootstrapController],
providers: [
{ provide: AUTH, useValue: mockAuth },
{ provide: DB, useValue: mockDbWithUser },
],
}).compile();
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
// Mirror main.ts configuration exactly — this is what reproduced the 400.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.init();
// Fastify requires waiting for the adapter to be ready
await app.getHttpAdapter().getInstance().ready();
});
afterAll(async () => {
await app.close();
});
it('returns 201 (not 400) when a valid {name, email, password} body is sent', async () => {
const res = await request(app.getHttpServer())
.post('/api/bootstrap/setup')
.send({ name: 'Admin', email: 'admin@example.com', password: 'password123' })
.set('Content-Type', 'application/json');
// Before the fix (import type), Nest ValidationPipe returned 400 with
// "property email should not exist" / "property password should not exist"
// because the DTO class was erased and every field looked non-whitelisted.
expect(res.status).not.toBe(400);
expect(res.status).toBe(201);
const body = res.body as BootstrapResultDto;
expect(body.user).toBeDefined();
expect(body.user.email).toBe('admin@example.com');
expect(body.token).toBeDefined();
expect(body.token.plaintext).toBeDefined();
});
it('returns 400 when extra forbidden properties are sent', async () => {
// This proves ValidationPipe IS active and working (forbidNonWhitelisted).
const res = await request(app.getHttpServer())
.post('/api/bootstrap/setup')
.send({
name: 'Admin',
email: 'admin@example.com',
password: 'password123',
extraField: 'should-be-rejected',
})
.set('Content-Type', 'application/json');
expect(res.status).toBe(400);
});
it('returns 400 when email is invalid', async () => {
const res = await request(app.getHttpServer())
.post('/api/bootstrap/setup')
.send({ name: 'Admin', email: 'not-an-email', password: 'password123' })
.set('Content-Type', 'application/json');
expect(res.status).toBe(400);
});
it('returns 400 when password is too short', async () => {
const res = await request(app.getHttpServer())
.post('/api/bootstrap/setup')
.send({ name: 'Admin', email: 'admin@example.com', password: 'short' })
.set('Content-Type', 'application/json');
expect(res.status).toBe(400);
});
});

View File

@@ -1,4 +1,3 @@
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
export default defineConfig({ export default defineConfig({
@@ -6,22 +5,4 @@ export default defineConfig({
globals: true, globals: true,
environment: 'node', environment: 'node',
}, },
plugins: [
swc.vite({
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
},
transform: {
decoratorMetadata: true,
legacyDecorator: true,
},
target: 'es2022',
},
module: {
type: 'nodenext',
},
}),
],
}); });

View File

@@ -7,11 +7,11 @@
**ID:** install-ux-v2-20260405 **ID:** install-ux-v2-20260405
**Statement:** The install-ux-hardening mission shipped the plumbing (uninstall, masked password, hooks consent, unified flow, headless path), but the first real end-to-end run surfaced a critical regression and a collection of UX failings that make the wizard feel neither quick nor intelligent. This mission closes the bootstrap regression as a hotfix, then rethinks the first-run experience around a provider-first, intent-driven flow with a drill-down main menu and a genuinely fast quick-start. **Statement:** The install-ux-hardening mission shipped the plumbing (uninstall, masked password, hooks consent, unified flow, headless path), but the first real end-to-end run surfaced a critical regression and a collection of UX failings that make the wizard feel neither quick nor intelligent. This mission closes the bootstrap regression as a hotfix, then rethinks the first-run experience around a provider-first, intent-driven flow with a drill-down main menu and a genuinely fast quick-start.
**Phase:** Execution **Phase:** Planning
**Current Milestone:** IUV-M03 **Current Milestone:** IUV-M01
**Progress:** 2 / 3 milestones **Progress:** 0 / 3 milestones
**Status:** active **Status:** active
**Last Updated:** 2026-04-05 (IUV-M02 complete — CORS/FQDN + skill installer rework) **Last Updated:** 2026-04-05
**Parent Mission:** [install-ux-hardening-20260405](./archive/missions/install-ux-hardening-20260405/MISSION-MANIFEST.md) (complete — `mosaic-v0.0.25`) **Parent Mission:** [install-ux-hardening-20260405](./archive/missions/install-ux-hardening-20260405/MISSION-MANIFEST.md) (complete — `mosaic-v0.0.25`)
## Context ## Context
@@ -30,11 +30,11 @@ Real-run testing of `@mosaicstack/mosaic@0.0.25` uncovered:
## Success Criteria ## Success Criteria
- [x] AC-1: Admin bootstrap completes successfully end-to-end on a fresh install (DTO value import, no forbidNonWhitelisted regression); covered by an integration or e2e test that exercises the real DTO binding. _(PR #440)_ - [ ] AC-1: Admin bootstrap completes successfully end-to-end on a fresh install (DTO value import, no forbidNonWhitelisted regression); covered by an integration or e2e test that exercises the real DTO binding.
- [x] AC-2: Wizard fails loudly (non-zero exit, clear error) when the bootstrap stage returns `completed: false`, in both interactive and headless modes. No more silent `✔ Wizard complete` after a 400. _(PR #440)_ - [ ] AC-2: Wizard fails loudly (non-zero exit, clear error) when the bootstrap stage returns `completed: false`, in both interactive and headless modes. No more silent `✔ Wizard complete` after a 400.
- [x] AC-3: Gateway port prompt prefills `14242` in the input field (user can press Enter to accept). _(PR #440)_ - [ ] AC-3: Gateway port prompt prefills `14242` in the input field (user can press Enter to accept).
- [x] AC-4: `"What is Mosaic?"` intro copy mentions Pi SDK as the underlying agent runtime. _(PR #440)_ - [ ] AC-4: `"What is Mosaic?"` intro copy mentions Pi SDK as the underlying agent runtime.
- [x] AC-5: Release `mosaic-v0.0.26` tagged and published to the Gitea npm registry, unblocking the 0.0.25 happy path. _(tag: mosaic-v0.0.26, registry: 0.0.26 live)_ - [ ] AC-5: Release `mosaic-v0.0.26` tagged and published to the Gitea npm registry, unblocking the 0.0.25 happy path.
- [ ] AC-6: CORS origin prompt replaced with FQDN/hostname input; CORS string is derived from that. - [ ] AC-6: CORS origin prompt replaced with FQDN/hostname input; CORS string is derived from that.
- [ ] AC-7: Skill / additional feature install section is reworked until it is actually usable end-to-end (worker defines the concrete failure modes during diagnosis). - [ ] AC-7: Skill / additional feature install section is reworked until it is actually usable end-to-end (worker defines the concrete failure modes during diagnosis).
- [ ] AC-8: First-run flow has a drill-down main menu with at least `Plugins` (Recommended / Custom), `Providers`, and the other top-level configuration groups. Linear interrogation is gone. - [ ] AC-8: First-run flow has a drill-down main menu with at least `Plugins` (Recommended / Custom), `Providers`, and the other top-level configuration groups. Linear interrogation is gone.
@@ -44,11 +44,11 @@ Real-run testing of `@mosaicstack/mosaic@0.0.25` uncovered:
## Milestones ## Milestones
| # | ID | Name | Status | Branch | Issue | Started | Completed | | # | ID | Name | Status | Branch | Issue | Started | Completed |
| --- | ------- | ------------------------------------------------------------ | ----------- | ---------------------- | ----- | ---------- | ---------- | | --- | ------- | ------------------------------------------------------------ | ----------- | ---------------------- | ----- | ---------- | --------- |
| 1 | IUV-M01 | Hotfix: bootstrap DTO + wizard failure + port prefill + copy | complete | fix/bootstrap-hotfix | #436 | 2026-04-05 | 2026-04-05 | | 1 | IUV-M01 | Hotfix: bootstrap DTO + wizard failure + port prefill + copy | in-progress | fix/bootstrap-hotfix | #436 | 2026-04-05 | |
| 2 | IUV-M02 | UX polish: CORS/FQDN, skill installer rework | complete | feat/install-ux-polish | #437 | 2026-04-05 | 2026-04-05 | | 2 | IUV-M02 | UX polish: CORS/FQDN, skill installer rework | not-started | feat/install-ux-polish | #437 | — | — |
| 3 | IUV-M03 | Provider-first intelligent flow + drill-down main menu | not-started | feat/install-ux-intent | #438 | — | — | | 3 | IUV-M03 | Provider-first intelligent flow + drill-down main menu | not-started | feat/install-ux-intent | #438 | — | — |
## Subagent Delegation Plan ## Subagent Delegation Plan

View File

@@ -9,29 +9,29 @@
## Milestone 1 — Hotfix: bootstrap DTO + wizard failure + port prefill + copy (IUV-M01) ## Milestone 1 — Hotfix: bootstrap DTO + wizard failure + port prefill + copy (IUV-M01)
| id | status | description | issue | agent | branch | depends_on | estimate | notes | | id | status | description | issue | agent | branch | depends_on | estimate | notes |
| --------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------ | -------------------- | ---------- | -------- | --------------------------------------------------------------------------------------- | | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------ | -------------------- | ---------- | -------- | ---------------------------------------------------------------------------------------------- |
| IUV-01-01 | done | Fix `apps/gateway/src/admin/bootstrap.controller.ts:16` — switch `import type { BootstrapSetupDto }` to a value import so Nest's `@Body()` binds the real class | #436 | sonnet | fix/bootstrap-hotfix | — | 3K | PR #440 merged `0ae932ab` | | IUV-01-01 | not-started | Fix `apps/gateway/src/admin/bootstrap.controller.ts:16` — switch `import type { BootstrapSetupDto }` to a value import so Nest's `@Body()` binds the real class | #436 | sonnet | fix/bootstrap-hotfix | — | 3K | one-character fix; repro is real-run `mosaic wizard` against `0.0.25` |
| IUV-01-02 | done | Add integration / e2e test that POSTs `/api/bootstrap/setup` with `{name,email,password}` against a real Nest app instance and asserts 201 — NOT a mocked controller unit test | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-01 | 10K | `apps/gateway/src/admin/bootstrap.e2e.spec.ts` — 4 tests; unplugin-swc added for vitest | | IUV-01-02 | not-started | Add integration / e2e test that POSTs `/api/bootstrap/setup` with `{name,email,password}` against a real Nest app instance and asserts 201 — NOT a mocked controller unit test | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-01 | 10K | must fail before the fix and pass after; guards against the class-erasure regression recurring |
| IUV-01-03 | done | `packages/mosaic/src/wizard.ts:147` — propagate `!bootstrapResult.completed` as a wizard failure in **interactive** mode too (not only headless); non-zero exit + no `✔ Wizard complete` line | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-02 | 5K | removed `&& headlessRun` guard | | IUV-01-03 | not-started | `packages/mosaic/src/wizard.ts:147` — propagate `!bootstrapResult.completed` as a wizard failure in **interactive** mode too (not only headless); non-zero exit + no `✔ Wizard complete` line | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-02 | 5K | |
| IUV-01-04 | done | Gateway port prompt prefills `14242` in the input buffer — investigate why `promptPort`'s `defaultValue` isn't reaching the user-visible input | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-03 | 5K | added `initialValue` through prompter interface → clack | | IUV-01-04 | not-started | Gateway port prompt prefills `14242` in the input buffer — investigate why `promptPort`'s `defaultValue` isn't reaching the user-visible input | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-03 | 5K | likely WizardPrompter adapter or @clack/prompts `initialValue` vs `defaultValue` mismatch |
| IUV-01-05 | done | `"What is Mosaic?"` intro copy updated to mention Pi SDK as the underlying agent runtime (alongside Claude Code / Codex / OpenCode) | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-04 | 2K | `packages/mosaic/src/stages/welcome.ts` | | IUV-01-05 | not-started | `"What is Mosaic?"` intro copy updated to mention Pi SDK as the underlying agent runtime (alongside Claude Code / Codex / OpenCode) | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-04 | 2K | |
| IUV-01-06 | done | Tests + code review + PR merge + tag `mosaic-v0.0.26` + Gitea release + npm registry republish | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-05 | 10K | PRs #440/#441/#442 merged; tag `mosaic-v0.0.26`; registry latest=0.0.26 ✓ | | IUV-01-06 | not-started | Tests + code review + PR merge + tag `mosaic-v0.0.26` + Gitea release + npm registry republish | #436 | sonnet | fix/bootstrap-hotfix | IUV-01-05 | 10K | bump `packages/mosaic/package.json` to 0.0.25 → 0.0.26 |
## Milestone 2 — UX polish: CORS/FQDN, skill installer rework (IUV-M02) ## Milestone 2 — UX polish: CORS/FQDN, skill installer rework (IUV-M02)
| id | status | description | issue | agent | branch | depends_on | estimate | notes | | id | status | description | issue | agent | branch | depends_on | estimate | notes |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------ | ----- | ------ | ---------------------- | ---------- | -------- | ---------------------------------------------------------------------- | | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----- | ------ | ---------------------- | ---------- | -------- | --------------------------- |
| IUV-02-01 | done | Replace CORS origin prompt with FQDN / hostname input; derive the CORS value internally; default to `localhost` with clear help text | #437 | sonnet | feat/install-ux-polish | | 10K | `deriveCorsOrigin()` pure fn; MOSAIC_HOSTNAME headless var; PR #444 | | IUV-02-01 | not-started | Replace CORS origin prompt with FQDN / hostname input; derive the CORS value internally; default to `localhost` with clear help text | #437 | sonnet | feat/install-ux-polish | IUV-01-06 | 10K | |
| IUV-02-02 | done | Diagnose and document the concrete failure modes of the current skill / additional feature install section end-to-end | #437 | sonnet | feat/install-ux-polish | IUV-02-01 | 8K | selection→install gap, silent catch{}, no whitelist concept | | IUV-02-02 | not-started | Diagnose and document the concrete failure modes of the current skill / additional feature install section end-to-end | #437 | sonnet | feat/install-ux-polish | IUV-02-01 | 8K | needs real-run reproduction |
| IUV-02-03 | done | Rework the skill installer so it is usable end-to-end (selection, install, verify, failure reporting) | #437 | sonnet | feat/install-ux-polish | IUV-02-02 | 20K | MOSAIC_INSTALL_SKILLS env var whitelist; SyncSkillsResult typed return | | IUV-02-03 | not-started | Rework the skill installer so it is usable end-to-end (selection, install, verify, failure reporting) | #437 | sonnet | feat/install-ux-polish | IUV-02-02 | 20K | |
| IUV-02-04 | done | Tests + code review + PR merge | #437 | sonnet | feat/install-ux-polish | IUV-02-03 | 10K | 18 new tests (13 CORS + 5 skills); PR #444 merged `172bacb3` | | IUV-02-04 | not-started | Tests + code review + PR merge | #437 | sonnet | feat/install-ux-polish | IUV-02-03 | 10K | |
## Milestone 3 — Provider-first intelligent flow + drill-down main menu (IUV-M03) ## Milestone 3 — Provider-first intelligent flow + drill-down main menu (IUV-M03)
| id | status | description | issue | agent | branch | depends_on | estimate | notes | | id | status | description | issue | agent | branch | depends_on | estimate | notes |
| --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | ----- | ---------------------- | ---------- | -------- | ------------------------------------------------------------- | | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | ----- | ---------------------- | ---------- | -------- | ------------------------------------------------------------- |
| IUV-03-01 | not-started | Design doc: new first-run state machine — main menu (Plugins / Providers / …), Quick Start vs Custom paths, provider-first flow, intent intake + naming loop | #438 | opus | feat/install-ux-intent | | 15K | scratchpad + explicit non-goals | | IUV-03-01 | not-started | Design doc: new first-run state machine — main menu (Plugins / Providers / …), Quick Start vs Custom paths, provider-first flow, intent intake + naming loop | #438 | opus | feat/install-ux-intent | IUV-02-04 | 15K | scratchpad + explicit non-goals |
| IUV-03-02 | not-started | Implement drill-down main menu (Plugins: Recommended / Custom, Providers, …) as the top-level entry point of `mosaic wizard` | #438 | opus | feat/install-ux-intent | IUV-03-01 | 25K | | | IUV-03-02 | not-started | Implement drill-down main menu (Plugins: Recommended / Custom, Providers, …) as the top-level entry point of `mosaic wizard` | #438 | opus | feat/install-ux-intent | IUV-03-01 | 25K | |
| IUV-03-03 | not-started | Quick Start path: curated minimum question set — define the exact baseline, delete everything else from the fast path | #438 | opus | feat/install-ux-intent | IUV-03-02 | 15K | | | IUV-03-03 | not-started | Quick Start path: curated minimum question set — define the exact baseline, delete everything else from the fast path | #438 | opus | feat/install-ux-intent | IUV-03-02 | 15K | |
| IUV-03-04 | not-started | Provider-first natural-language intake: user describes intent → agent expounds → agent proposes a name (confirmable / overridable) — OpenClaw-style | #438 | opus | feat/install-ux-intent | IUV-03-03 | 25K | offline fallback required (deterministic default name + path) | | IUV-03-04 | not-started | Provider-first natural-language intake: user describes intent → agent expounds → agent proposes a name (confirmable / overridable) — OpenClaw-style | #438 | opus | feat/install-ux-intent | IUV-03-03 | 25K | offline fallback required (deterministic default name + path) |

View File

@@ -165,13 +165,7 @@ The `mosaic` CLI provides a terminal interface to the same gateway API.
Install via the Mosaic installer: Install via the Mosaic installer:
```bash ```bash
curl -fsSL https://mosaicstack.dev/install.sh | bash bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
```
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

View File

@@ -107,67 +107,3 @@ Sequencing: strict. M01 ships first as a hotfix release (mosaic-v0.0.26). M02 is
1. Create Gitea issues for M01, M02, M03 1. Create Gitea issues for M01, M02, M03
2. Open the mission-scaffold docs PR (same pattern as parent mission's PR #430) 2. Open the mission-scaffold docs PR (same pattern as parent mission's PR #430)
3. After merge, delegate IUV-M01 to a sonnet subagent in an isolated worktree with the concrete fix-site pointers above 3. After merge, delegate IUV-M01 to a sonnet subagent in an isolated worktree with the concrete fix-site pointers above
## Session 2 — 2026-04-05 (IUV-M01 delivery + close-out)
### Outcome
IUV-M01 shipped. `mosaic-v0.0.26` released and registry latest confirmed `0.0.26`.
### PRs merged
| PR | Title | Merge |
| ---- | ------------------------------------------------------------------------ | -------- |
| #440 | fix: bootstrap hotfix — DTO erasure, wizard failure, port prefill, copy | 0ae932ab |
| #441 | fix: add vitest.config.ts to eslint allowDefaultProject (#440 build fix) | c08aa6fa |
| #442 | docs: mark IUV-M01 complete — mosaic-v0.0.26 released | 78388437 |
### Bugs fixed (all 4 in worker's PR #440)
1. **DTO class erasure**`apps/gateway/src/admin/bootstrap.controller.ts:16` — dropped `type` from `import { BootstrapSetupDto }`. Guarded by new e2e test `bootstrap.e2e.spec.ts` (4 cases) that binds through a real Nest app with `ValidationPipe { whitelist, forbidNonWhitelisted }`. Test suite needed `unplugin-swc` in `apps/gateway/vitest.config.ts` to emit `decoratorMetadata` (tsx/esbuild can't).
2. **Wizard silent failure**`packages/mosaic/src/wizard.ts` — removed the `&& headlessRun` guard so `!bootstrapResult.completed` now aborts in both modes.
3. **Port prefill** — root cause was clack's `defaultValue` vs `initialValue` semantics (`defaultValue` only fills on empty submit, `initialValue` prefills the buffer). Added an `initialValue` field to `WizardPrompter.text()` interface, threaded through clack and headless prompters, switched `gateway-config.ts` port/url prompts to use it.
4. **Pi SDK copy**`packages/mosaic/src/stages/welcome.ts` — intro copy now lists Pi SDK.
### Mid-delivery hiccup — tsconfig/eslint cross-contamination
Worker's initial approach added `vitest.config.ts` to `apps/gateway/tsconfig.json`'s `include` to appease the eslint parser. That broke `pnpm --filter @mosaicstack/gateway build` with TS6059 (`vitest.config.ts` outside `rootDir: "src"`). The publish pipeline on the `#440` merge commit failed.
**Correct fix** (worker's PR #441): leave `tsconfig.json` clean (`include: ["src/**/*"]`) and instead add the file to `allowDefaultProject` in the root `eslint.config.mjs`. This keeps the tsc program strict while letting eslint resolve a parser project for the standalone config file.
**Pattern to remember**: when adding root-level `.ts` config files (vitest, build scripts) to a package with `rootDir: "src"`, the eslint parser project conflict is solved with `allowDefaultProject`, NEVER by widening tsconfig include. I had independently arrived at the same fix on a branch before the worker shipped #441 — deleted the duplicate.
### Residual follow-ups carried forward
1. Headless prompter fallback order: worker set `initialValue > defaultValue` in the headless path. Correct semantic, but any future headless test that explicitly depends on `defaultValue` precedence will need review.
2. Vitest + SWC decorator metadata pattern is now the blessed approach for NestJS e2e tests in this monorepo. Any other package that adds NestJS e2e tests should mirror `apps/gateway/vitest.config.ts`.
### Next action
- Close out orchestrator doc sync (this commit): mark M01 subtasks done in `TASKS.md`, update manifest phase to Execution, commit scratchpad session 2, PR to main.
- After merge, delegate IUV-M02 (sonnet, isolated worktree). Dependencies: IUV-02-01 (CORS→FQDN) starts unblocked since M01 is released; first real task for the M02 worker is diagnosing the skill installer failure modes (IUV-02-02) against the fresh 0.0.26 install.
## Session 3 — 2026-04-05 (IUV-M02 delivery + close-out)
### Outcome
IUV-M02 shipped. PR #444 merged (`172bacb3`), issue #437 closed. 18 new tests (13 CORS derivation, 5 skill sync).
### Changes
**CORS → FQDN (IUV-02-01):**
- `packages/mosaic/src/stages/gateway-config.ts` — replaced raw "CORS origin" text prompt with "Web UI hostname" (default: `localhost`). Added HTTPS follow-up for remote hosts. Pure `deriveCorsOrigin(hostname, port, useHttps?)` function exported for testability.
- Headless: `MOSAIC_HOSTNAME` env var as friendly alternative; `MOSAIC_CORS_ORIGIN` still works as full override.
- `packages/mosaic/src/types.ts` — added `hostname?: string` to `GatewayState`.
**Skill installer rework (IUV-02-02 + IUV-02-03):**
- Root cause confirmed: `syncSkills()` in `finalize.ts` ignored `state.selectedSkills` entirely. The multiselect UI was a no-op.
- `packages/mosaic/src/stages/finalize.ts``syncSkills()` rewritten to accept `selectedSkills[]`, returns typed `SyncSkillsResult`, passes `MOSAIC_INSTALL_SKILLS` (colon-separated) as env var to the bash script.
- `packages/mosaic/framework/tools/_scripts/mosaic-sync-skills` — added bash associative array whitelist filter keyed on `MOSAIC_INSTALL_SKILLS`. When set, only whitelisted skills are linked. Empty/unset = all skills (legacy behavior preserved for `mosaic sync` outside wizard).
- Failure surfaces: silent `catch {}` replaced with typed error reporting through `p.warn()`.
### Next action
- Delegate IUV-M03 (opus, isolated worktree) — the architectural milestone: provider-first intelligent flow, drill-down main menu, Quick Start fast path, agent self-naming. This is the biggest piece of the mission.

View File

@@ -1,227 +0,0 @@
# 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.

View File

@@ -27,7 +27,6 @@ export default tseslint.config(
'apps/web/e2e/*.ts', 'apps/web/e2e/*.ts',
'apps/web/e2e/helpers/*.ts', 'apps/web/e2e/helpers/*.ts',
'apps/web/playwright.config.ts', 'apps/web/playwright.config.ts',
'apps/gateway/vitest.config.ts',
'packages/mosaic/__tests__/*.ts', 'packages/mosaic/__tests__/*.ts',
], ],
}, },

View File

@@ -73,27 +73,6 @@ 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:

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/agent" "directory": "packages/agent"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/auth" "directory": "packages/auth"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.3", "version": "0.0.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/brain" "directory": "packages/brain"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/config" "directory": "packages/config"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/coord" "directory": "packages/coord"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.3", "version": "0.0.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/db" "directory": "packages/db"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/design-tokens" "directory": "packages/design-tokens"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.3", "version": "0.0.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/forge" "directory": "packages/forge"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.3", "version": "0.0.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/log" "directory": "packages/log"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.3", "version": "0.0.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/macp" "directory": "packages/macp"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.4", "version": "0.0.4",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/memory" "directory": "packages/memory"
}, },
"type": "module", "type": "module",

View File

@@ -17,7 +17,6 @@ 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-'));
@@ -33,16 +32,12 @@ 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',
@@ -67,10 +62,9 @@ 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',

View File

@@ -151,68 +151,11 @@ 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
- Load skills that match the active task domain before starting implementation. - Use only the minimum required skills for the active task.
- 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

View File

@@ -4,20 +4,14 @@ 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/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. > **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.
## Quick Install ## Quick Install
### Mac / Linux ### Mac / Linux
```bash ```bash
curl -fsSL https://mosaicstack.dev/install.sh | bash bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
```
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)
@@ -29,8 +23,8 @@ bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/
### From Source (any platform) ### From Source (any platform)
```bash ```bash
git clone git@git.mosaicstack.dev:mosaicstack/stack.git ~/src/stack git clone git@git.mosaicstack.dev:mosaic/mosaic-stack.git ~/src/mosaic-stack
cd ~/src/stack && bash tools/install.sh cd ~/src/mosaic-stack && bash tools/install.sh
``` ```
The installer: The installer:
@@ -151,19 +145,13 @@ 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
curl -fsSL https://mosaicstack.dev/install.sh | bash bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
```
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/stack && git pull && bash tools/install.sh cd ~/src/mosaic-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.

View File

@@ -19,9 +19,8 @@ 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/dirs preserved across upgrades (never overwritten). # Files preserved across upgrades (never overwritten)
# User-created content in these paths survives rsync --delete. PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory" "sources")
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.
@@ -218,22 +217,8 @@ fi
sync_framework sync_framework
# Ensure persistent directories exist # Ensure memory directory exists
mkdir -p "$TARGET_DIR/memory" mkdir -p "$TARGET_DIR/memory"
mkdir -p "$TARGET_DIR/credentials"
# Seed defaults — copy from defaults/ to framework root if not already present.
# These are user-editable files that ship with sensible defaults but should
# never be overwritten once the user has customized them.
DEFAULTS_DIR="$TARGET_DIR/defaults"
if [[ -d "$DEFAULTS_DIR" ]]; then
for default_file in AGENTS.md STANDARDS.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

View File

@@ -102,30 +102,3 @@ 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.

View File

@@ -23,16 +23,6 @@
"timeout": 60 "timeout": 60
} }
] ]
},
{
"matcher": "Edit|MultiEdit|Write",
"hooks": [
{
"type": "command",
"command": "~/.config/mosaic/tools/qa/typecheck-hook.sh",
"timeout": 30
}
]
} }
] ]
}, },

View File

@@ -7,11 +7,6 @@ SKILLS_REPO_DIR="${MOSAIC_SKILLS_REPO_DIR:-$MOSAIC_HOME/sources/agent-skills}"
MOSAIC_SKILLS_DIR="$MOSAIC_HOME/skills" MOSAIC_SKILLS_DIR="$MOSAIC_HOME/skills"
MOSAIC_LOCAL_SKILLS_DIR="$MOSAIC_HOME/skills-local" MOSAIC_LOCAL_SKILLS_DIR="$MOSAIC_HOME/skills-local"
# Colon-separated list of skill names to install. When set, only these skills
# are linked into runtime skill directories. Empty/unset = link all skills
# (the legacy "mosaic sync" full-catalog behavior).
MOSAIC_INSTALL_SKILLS="${MOSAIC_INSTALL_SKILLS:-}"
fetch=1 fetch=1
link_only=0 link_only=0
@@ -30,7 +25,6 @@ Env:
MOSAIC_HOME Default: ~/.config/mosaic MOSAIC_HOME Default: ~/.config/mosaic
MOSAIC_SKILLS_REPO_URL Default: https://git.mosaicstack.dev/mosaic/agent-skills.git MOSAIC_SKILLS_REPO_URL Default: https://git.mosaicstack.dev/mosaic/agent-skills.git
MOSAIC_SKILLS_REPO_DIR Default: ~/.config/mosaic/sources/agent-skills MOSAIC_SKILLS_REPO_DIR Default: ~/.config/mosaic/sources/agent-skills
MOSAIC_INSTALL_SKILLS Colon-separated list of skills to link (default: all)
USAGE USAGE
} }
@@ -162,27 +156,6 @@ link_targets=(
canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")" canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")"
# Build an associative array from the colon-separated whitelist for O(1) lookup.
# When MOSAIC_INSTALL_SKILLS is empty, all skills are allowed.
declare -A _skill_whitelist=()
_whitelist_active=0
if [[ -n "$MOSAIC_INSTALL_SKILLS" ]]; then
_whitelist_active=1
IFS=':' read -ra _wl_items <<< "$MOSAIC_INSTALL_SKILLS"
for _item in "${_wl_items[@]}"; do
[[ -n "$_item" ]] && _skill_whitelist["$_item"]=1
done
fi
is_skill_selected() {
local name="$1"
if [[ $_whitelist_active -eq 0 ]]; then
return 0
fi
[[ -n "${_skill_whitelist[$name]:-}" ]] && return 0
return 1
}
link_skill_into_target() { link_skill_into_target() {
local skill_path="$1" local skill_path="$1"
local target_dir="$2" local target_dir="$2"
@@ -195,11 +168,6 @@ link_skill_into_target() {
return return
fi fi
# Respect the install whitelist (set during first-run wizard).
if ! is_skill_selected "$name"; then
return
fi
link_path="$target_dir/$name" link_path="$target_dir/$name"
if [[ -L "$link_path" ]]; then if [[ -L "$link_path" ]]; then

View File

@@ -1,63 +0,0 @@
#!/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

View File

@@ -1,9 +1,9 @@
{ {
"name": "@mosaicstack/mosaic", "name": "@mosaicstack/mosaic",
"version": "0.0.28", "version": "0.0.25",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-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",

View File

@@ -135,11 +135,15 @@ program
// No valid session — prompt for credentials // No valid session — prompt for credentials
if (!session) { if (!session) {
const { promptLine, promptSecret } = await import('./commands/gateway/login.js'); const readline = await import('node:readline');
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 promptLine('Email: '); const email = await ask('Email: ');
const password = await promptSecret('Password: '); const password = await ask('Password: ');
rl.close();
try { try {
const auth = await signIn(opts.gateway, email, password); const auth = await signIn(opts.gateway, email, password);

View File

@@ -78,82 +78,6 @@ 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
@@ -483,10 +407,6 @@ 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);

View File

@@ -26,53 +26,6 @@ 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',

View File

@@ -39,7 +39,6 @@ export class ClackPrompter implements WizardPrompter {
message: string; message: string;
placeholder?: string; placeholder?: string;
defaultValue?: string; defaultValue?: string;
initialValue?: string;
validate?: (value: string) => string | void; validate?: (value: string) => string | void;
}): Promise<string> { }): Promise<string> {
const validate = opts.validate const validate = opts.validate
@@ -52,7 +51,6 @@ export class ClackPrompter implements WizardPrompter {
message: opts.message, message: opts.message,
placeholder: opts.placeholder, placeholder: opts.placeholder,
defaultValue: opts.defaultValue, defaultValue: opts.defaultValue,
initialValue: opts.initialValue,
validate, validate,
}); });
return guardCancel(result); return guardCancel(result);

View File

@@ -35,18 +35,15 @@ export class HeadlessPrompter implements WizardPrompter {
message: string; message: string;
placeholder?: string; placeholder?: string;
defaultValue?: string; defaultValue?: string;
initialValue?: string;
validate?: (value: string) => string | void; validate?: (value: string) => string | void;
}): Promise<string> { }): Promise<string> {
const answer = this.answers.get(opts.message); const answer = this.answers.get(opts.message);
const value = const value =
typeof answer === 'string' typeof answer === 'string'
? answer ? answer
: opts.initialValue !== undefined : opts.defaultValue !== undefined
? opts.initialValue ? opts.defaultValue
: opts.defaultValue !== undefined : undefined;
? opts.defaultValue
: undefined;
if (value === undefined) { if (value === undefined) {
throw new Error(`HeadlessPrompter: no answer for "${opts.message}"`); throw new Error(`HeadlessPrompter: no answer for "${opts.message}"`);

View File

@@ -24,8 +24,6 @@ export interface WizardPrompter {
message: string; message: string;
placeholder?: string; placeholder?: string;
defaultValue?: string; defaultValue?: string;
/** Prefills the input buffer so the user sees the value and can press Enter to accept. */
initialValue?: string;
validate?: (value: string) => string | void; validate?: (value: string) => string | void;
}): Promise<string>; }): Promise<string>;

View File

@@ -1,129 +0,0 @@
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');
});
});

View File

@@ -1,64 +0,0 @@
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}`);
}

View File

@@ -1,186 +0,0 @@
/**
* Tests for the skill installer rework (IUV-02-03).
*
* We mock `node:child_process` to verify that:
* 1. syncSkills passes MOSAIC_INSTALL_SKILLS with the exact selected subset
* 2. When the script exits non-zero, the failure is surfaced to the user
* 3. When the script is missing, a clear error is shown (not a silent no-op)
* 4. An empty selection is a no-op (script never called)
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import type { WizardState } from '../types.js';
import type { ConfigService } from '../config/config-service.js';
// ── spawnSync mock ─────────────────────────────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const spawnSyncMock = vi.fn<any>();
vi.mock('node:child_process', () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
spawnSync: (...args: any[]) => spawnSyncMock(...args),
}));
// ── platform stub ──────────────────────────────────────────────────────────────
vi.mock('../platform/detect.js', () => ({
getShellProfilePath: () => null,
}));
import { finalizeStage } from './finalize.js';
// ── Helpers ────────────────────────────────────────────────────────────────────
function makeState(mosaicHome: string, selectedSkills: string[] = []): WizardState {
return {
mosaicHome,
sourceDir: mosaicHome,
mode: 'quick',
installAction: 'fresh',
soul: { agentName: 'TestBot', communicationStyle: 'direct' },
user: {},
tools: {},
runtimes: { detected: [], mcpConfigured: false },
selectedSkills,
};
}
function buildPrompter() {
return {
intro: vi.fn(),
outro: vi.fn(),
note: vi.fn(),
log: vi.fn(),
warn: vi.fn(),
text: vi.fn(),
confirm: vi.fn(),
select: vi.fn(),
multiselect: vi.fn(),
groupMultiselect: vi.fn(),
spinner: vi.fn().mockReturnValue({ update: vi.fn(), stop: vi.fn() }),
separator: vi.fn(),
};
}
function makeConfigService(): ConfigService {
return {
readSoul: vi.fn().mockResolvedValue({}),
readUser: vi.fn().mockResolvedValue({}),
readTools: vi.fn().mockResolvedValue({}),
writeSoul: vi.fn().mockResolvedValue(undefined),
writeUser: vi.fn().mockResolvedValue(undefined),
writeTools: vi.fn().mockResolvedValue(undefined),
syncFramework: vi.fn().mockResolvedValue(undefined),
get: vi.fn(),
set: vi.fn(),
getSection: vi.fn(),
} as unknown as ConfigService;
}
// ── Tests ──────────────────────────────────────────────────────────────────────
describe('finalizeStage — skill installer', () => {
let tmp: string;
let binDir: string;
let syncScript: string;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), 'mosaic-finalize-'));
binDir = join(tmp, 'bin');
mkdirSync(binDir, { recursive: true });
syncScript = join(binDir, 'mosaic-sync-skills');
// Default: script exists and succeeds
writeFileSync(syncScript, '#!/usr/bin/env bash\necho ok\n', { mode: 0o755 });
spawnSyncMock.mockReturnValue({ status: 0, stdout: 'ok', stderr: '' });
});
afterEach(() => {
rmSync(tmp, { recursive: true, force: true });
vi.clearAllMocks();
});
function findSkillsSyncCall() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (spawnSyncMock.mock.calls as any[][]).find(
(args) =>
Array.isArray(args[1]) &&
(args[1] as string[]).some((a) => a.includes('mosaic-sync-skills')),
);
}
it('passes MOSAIC_INSTALL_SKILLS with the selected skill list', async () => {
const state = makeState(tmp, ['brainstorming', 'lint', 'systematic-debugging']);
const p = buildPrompter();
const config = makeConfigService();
await finalizeStage(p, state, config);
const call = findSkillsSyncCall();
expect(call).toBeDefined();
const opts = call![2] as { env?: Record<string, string> };
expect(opts.env?.['MOSAIC_INSTALL_SKILLS']).toBe('brainstorming:lint:systematic-debugging');
});
it('skips the sync script entirely when no skills are selected', async () => {
const state = makeState(tmp, []);
const p = buildPrompter();
const config = makeConfigService();
await finalizeStage(p, state, config);
expect(findSkillsSyncCall()).toBeUndefined();
});
it('warns the user when the sync script exits non-zero', async () => {
spawnSyncMock.mockReturnValue({
status: 1,
stdout: '',
stderr: 'git clone failed: connection refused',
});
const state = makeState(tmp, ['brainstorming']);
const p = buildPrompter();
const config = makeConfigService();
await finalizeStage(p, state, config);
expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('git clone failed'));
expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('mosaic sync'));
});
it('warns the user when the sync script is missing', async () => {
// Remove the script to simulate a missing installation
rmSync(syncScript);
const state = makeState(tmp, ['brainstorming']);
const p = buildPrompter();
const config = makeConfigService();
await finalizeStage(p, state, config);
// spawnSync should NOT have been called for the skills script
expect(findSkillsSyncCall()).toBeUndefined();
expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('not found'));
});
it('includes skills count in the summary when install succeeds', async () => {
const state = makeState(tmp, ['brainstorming', 'lint']);
const p = buildPrompter();
const config = makeConfigService();
await finalizeStage(p, state, config);
const noteMock = p.note as ReturnType<typeof vi.fn>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const summaryCall = (noteMock.mock.calls as any[][]).find(
([, title]) => title === 'Installation Summary',
);
expect(summaryCall).toBeDefined();
expect(summaryCall![0] as string).toContain('2 installed');
});
});

View File

@@ -25,68 +25,14 @@ function linkRuntimeAssets(mosaicHome: string, skipClaudeHooks: boolean): void {
} }
} }
interface SyncSkillsResult { function syncSkills(mosaicHome: string): void {
success: boolean;
installedCount: number;
failureReason?: string;
}
/**
* Sync skills from the catalog and link only the user-selected subset.
*
* When `selectedSkills` is non-empty the script receives the list via
* `MOSAIC_INSTALL_SKILLS` (colon-separated) so it can skip unlisted skills
* during the linking phase. An empty selection is a no-op.
*
* Failure modes surfaced here:
* - Script not found → tells the user explicitly
* - Script exits non-zero → stderr is captured and reported
* - Catalog directory missing → detected before exec, reported clearly
*/
function syncSkills(mosaicHome: string, selectedSkills: string[]): SyncSkillsResult {
if (selectedSkills.length === 0) {
return { success: true, installedCount: 0 };
}
const script = join(mosaicHome, 'bin', 'mosaic-sync-skills'); const script = join(mosaicHome, 'bin', 'mosaic-sync-skills');
if (!existsSync(script)) { if (existsSync(script)) {
return { try {
success: false, spawnSync('bash', [script], { timeout: 60000, stdio: 'pipe' });
installedCount: 0, } catch {
failureReason: `Skills sync script not found at ${script} — run 'mosaic sync' after installation.`, // Non-fatal
};
}
try {
const result = spawnSync('bash', [script], {
timeout: 60000,
stdio: 'pipe',
encoding: 'utf-8',
env: {
...process.env,
MOSAIC_HOME: mosaicHome,
MOSAIC_INSTALL_SKILLS: selectedSkills.join(':'),
},
});
if (result.status !== 0) {
const stderr = (result.stderr ?? '').trim();
return {
success: false,
installedCount: 0,
failureReason: stderr
? `Skills sync failed: ${stderr}`
: `Skills sync script exited with code ${(result.status ?? 'unknown').toString()}`,
};
} }
return { success: true, installedCount: selectedSkills.length };
} catch (err) {
return {
success: false,
installedCount: 0,
failureReason: `Skills sync threw: ${err instanceof Error ? err.message : String(err)}`,
};
} }
} }
@@ -178,11 +124,10 @@ export async function finalizeStage(
const skipClaudeHooks = state.hooks?.accepted === false; const skipClaudeHooks = state.hooks?.accepted === false;
linkRuntimeAssets(state.mosaicHome, skipClaudeHooks); linkRuntimeAssets(state.mosaicHome, skipClaudeHooks);
// 4. Sync skills (only installs the user-selected subset) // 4. Sync skills
let skillsResult: SyncSkillsResult = { success: true, installedCount: 0 };
if (state.selectedSkills.length > 0) { if (state.selectedSkills.length > 0) {
spin.update(`Installing ${state.selectedSkills.length.toString()} selected skill(s)...`); spin.update('Syncing skills...');
skillsResult = syncSkills(state.mosaicHome, state.selectedSkills); syncSkills(state.mosaicHome);
} }
// 5. Run doctor // 5. Run doctor
@@ -191,27 +136,15 @@ export async function finalizeStage(
spin.stop('Installation complete'); spin.stop('Installation complete');
// Report skill install failure clearly (non-fatal but user should know)
if (!skillsResult.success && skillsResult.failureReason) {
p.warn(skillsResult.failureReason);
p.warn("Run 'mosaic sync' manually after installation to install skills.");
}
// 6. PATH setup // 6. PATH setup
const pathAction = setupPath(state.mosaicHome, p); const pathAction = setupPath(state.mosaicHome, p);
// 7. Summary // 7. Summary
const skillsSummary = skillsResult.success
? skillsResult.installedCount > 0
? `${skillsResult.installedCount.toString()} installed`
: 'none selected'
: `install failed — ${skillsResult.failureReason ?? 'unknown error'}`;
const summary: string[] = [ const summary: string[] = [
`Agent: ${state.soul.agentName ?? 'Assistant'}`, `Agent: ${state.soul.agentName ?? 'Assistant'}`,
`Style: ${state.soul.communicationStyle ?? 'direct'}`, `Style: ${state.soul.communicationStyle ?? 'direct'}`,
`Runtimes: ${state.runtimes.detected.join(', ') || 'none detected'}`, `Runtimes: ${state.runtimes.detected.join(', ') || 'none detected'}`,
`Skills: ${skillsSummary}`, `Skills: ${state.selectedSkills.length.toString()} selected`,
`Config: ${state.mosaicHome}`, `Config: ${state.mosaicHome}`,
]; ];

View File

@@ -158,7 +158,7 @@ export async function gatewayBootstrapStage(
host, host,
port, port,
tier: 'local', tier: 'local',
corsOrigin: `http://${host}:3000`, corsOrigin: 'http://localhost:3000',
}), }),
admin: { name, email, password }, admin: { name, email, password },
}; };

View File

@@ -1,69 +0,0 @@
import { describe, it, expect } from 'vitest';
import { deriveCorsOrigin } from './gateway-config.js';
describe('deriveCorsOrigin', () => {
describe('localhost / loopback — always http', () => {
it('localhost port 3000 → http://localhost:3000', () => {
expect(deriveCorsOrigin('localhost', 3000)).toBe('http://localhost:3000');
});
it('127.0.0.1 port 3000 → http://127.0.0.1:3000', () => {
expect(deriveCorsOrigin('127.0.0.1', 3000)).toBe('http://127.0.0.1:3000');
});
it('localhost port 80 omits port suffix', () => {
expect(deriveCorsOrigin('localhost', 80)).toBe('http://localhost');
});
it('localhost port 443 still uses http (loopback overrides), includes port', () => {
// 443 is the https default port, but since localhost forces http, the port
// is NOT the default for http (80), so it must be included.
expect(deriveCorsOrigin('localhost', 443)).toBe('http://localhost:443');
});
it('useHttps=false on localhost keeps http', () => {
expect(deriveCorsOrigin('localhost', 3000, false)).toBe('http://localhost:3000');
});
it('useHttps=true on localhost still uses http (loopback wins)', () => {
// Passing useHttps=true for localhost is unusual but the function honours
// the explicit override — loopback detection only applies when useHttps is
// undefined (auto-detect path).
expect(deriveCorsOrigin('localhost', 3000, true)).toBe('https://localhost:3000');
});
});
describe('remote hostname — defaults to https', () => {
it('example.com port 3000 → https://example.com:3000', () => {
expect(deriveCorsOrigin('example.com', 3000)).toBe('https://example.com:3000');
});
it('example.com port 443 omits port suffix', () => {
expect(deriveCorsOrigin('example.com', 443)).toBe('https://example.com');
});
it('example.com port 80 → https://example.com:80 (non-default port for https)', () => {
expect(deriveCorsOrigin('example.com', 80)).toBe('https://example.com:80');
});
it('useHttps=false on remote host uses http', () => {
expect(deriveCorsOrigin('example.com', 3000, false)).toBe('http://example.com:3000');
});
it('useHttps=false on remote host, port 80 omits suffix', () => {
expect(deriveCorsOrigin('example.com', 80, false)).toBe('http://example.com');
});
});
describe('subdomain and non-standard hostnames', () => {
it('sub.domain.example.com defaults to https', () => {
expect(deriveCorsOrigin('sub.domain.example.com', 3000)).toBe(
'https://sub.domain.example.com:3000',
);
});
it('myserver.local defaults to https (not loopback)', () => {
expect(deriveCorsOrigin('myserver.local', 8080)).toBe('https://myserver.local:8080');
});
});
});

View File

@@ -26,25 +26,6 @@ function isHeadless(): boolean {
return process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY; return process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
} }
// ── CORS derivation ───────────────────────────────────────────────────────────
/**
* Derive a full CORS origin URL from a user-provided hostname + web UI port.
*
* Rules:
* - "localhost" and "127.0.0.1" always use http (never https)
* - Everything else uses https by default; pass useHttps=false to override
* - Standard ports (80 for http, 443 for https) are omitted from the origin
*/
export function deriveCorsOrigin(hostname: string, webUiPort: number, useHttps?: boolean): string {
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1';
const proto =
useHttps !== undefined ? (useHttps ? 'https' : 'http') : isLocalhost ? 'http' : 'https';
const defaultPort = proto === 'https' ? 443 : 80;
const portSuffix = webUiPort === defaultPort ? '' : `:${webUiPort.toString()}`;
return `${proto}://${hostname}${portSuffix}`;
}
// ── .env helpers ────────────────────────────────────────────────────────────── // ── .env helpers ──────────────────────────────────────────────────────────────
function readEnvVarFromFile(envFile: string, key: string): string | null { function readEnvVarFromFile(envFile: string, key: string): string | null {
@@ -96,10 +77,6 @@ async function promptTier(p: WizardPrompter): Promise<GatewayStorageTier> {
async function promptPort(p: WizardPrompter, defaultPort: number): Promise<number> { async function promptPort(p: WizardPrompter, defaultPort: number): Promise<number> {
const raw = await p.text({ const raw = await p.text({
message: 'Gateway port', message: 'Gateway port',
// initialValue prefills the input buffer so the user sees 14242 and can
// press Enter to accept it. defaultValue is only used when the user submits
// an empty string, which never shows in the field.
initialValue: defaultPort.toString(),
defaultValue: defaultPort.toString(), defaultValue: defaultPort.toString(),
validate: (v) => { validate: (v) => {
const n = parseInt(v, 10); const n = parseInt(v, 10);
@@ -126,14 +103,6 @@ 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 {
@@ -255,9 +224,7 @@ export async function gatewayConfigStage(
host: existing.host, host: existing.host,
port: existing.port, port: existing.port,
tier: 'local', tier: 'local',
corsOrigin: corsOrigin: 'http://localhost:3000',
readEnvVarFromFile(ENV_FILE, 'GATEWAY_CORS_ORIGIN') ??
deriveCorsOrigin('localhost', 3000),
regeneratedConfig: false, regeneratedConfig: false,
}; };
return { ready: true, host: existing.host, port: existing.port }; return { ready: true, host: existing.host, port: existing.port };
@@ -310,8 +277,7 @@ export async function gatewayConfigStage(
host, host,
port, port,
tier: 'local', tier: 'local',
corsOrigin: corsOrigin: 'http://localhost:3000',
readEnvVarFromFile(ENV_FILE, 'GATEWAY_CORS_ORIGIN') ?? deriveCorsOrigin('localhost', 3000),
regeneratedConfig: false, regeneratedConfig: false,
}; };
} else { } else {
@@ -322,8 +288,6 @@ 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) {
@@ -399,10 +363,6 @@ 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. */
@@ -431,7 +391,6 @@ async function collectAndWriteConfig(
let valkeyUrl: string | undefined; let valkeyUrl: string | undefined;
let anthropicKey: string; let anthropicKey: string;
let corsOrigin: string; let corsOrigin: string;
let hostname: string;
if (isHeadless()) { if (isHeadless()) {
p.log('Headless mode detected — reading configuration from environment variables.'); p.log('Headless mode detected — reading configuration from environment variables.');
@@ -445,13 +404,7 @@ async function collectAndWriteConfig(
databaseUrl = process.env['MOSAIC_DATABASE_URL']; databaseUrl = process.env['MOSAIC_DATABASE_URL'];
valkeyUrl = process.env['MOSAIC_VALKEY_URL']; valkeyUrl = process.env['MOSAIC_VALKEY_URL'];
anthropicKey = process.env['MOSAIC_ANTHROPIC_API_KEY'] ?? ''; anthropicKey = process.env['MOSAIC_ANTHROPIC_API_KEY'] ?? '';
corsOrigin = process.env['MOSAIC_CORS_ORIGIN'] ?? 'http://localhost:3000';
// MOSAIC_CORS_ORIGIN is the full override (e.g. from CI).
// MOSAIC_HOSTNAME is the user-friendly alternative — derive from it.
const corsOverride = process.env['MOSAIC_CORS_ORIGIN'];
const hostnameEnv = process.env['MOSAIC_HOSTNAME'] ?? 'localhost';
hostname = hostnameEnv;
corsOrigin = corsOverride ?? deriveCorsOrigin(hostnameEnv, 3000);
if (tier === 'team') { if (tier === 'team') {
const missing: string[] = []; const missing: string[] = [];
@@ -470,44 +423,23 @@ async function collectAndWriteConfig(
if (tier === 'team') { if (tier === 'team') {
databaseUrl = await p.text({ databaseUrl = await p.text({
message: 'DATABASE_URL', message: 'DATABASE_URL',
initialValue: 'postgresql://mosaic:mosaic@localhost:5433/mosaic',
defaultValue: 'postgresql://mosaic:mosaic@localhost:5433/mosaic', defaultValue: 'postgresql://mosaic:mosaic@localhost:5433/mosaic',
}); });
valkeyUrl = await p.text({ valkeyUrl = await p.text({
message: 'VALKEY_URL', message: 'VALKEY_URL',
initialValue: 'redis://localhost:6380',
defaultValue: 'redis://localhost:6380', defaultValue: 'redis://localhost:6380',
}); });
} }
if (opts.providerKey) { anthropicKey = await p.text({
anthropicKey = opts.providerKey; message: 'ANTHROPIC_API_KEY (optional, press Enter to skip)',
p.log(`Using API key from provider setup (${opts.providerType ?? 'unknown'}).`); defaultValue: '',
} else {
anthropicKey = await p.text({
message: 'ANTHROPIC_API_KEY (optional, press Enter to skip)',
defaultValue: '',
});
}
hostname = await p.text({
message: 'Web UI hostname (for browser access)',
initialValue: 'localhost',
defaultValue: 'localhost',
placeholder: 'e.g. localhost or myserver.example.com',
}); });
// For non-localhost, ask if HTTPS is in use (defaults to yes for remote hosts) corsOrigin = await p.text({
let useHttps: boolean | undefined; message: 'CORS origin',
if (hostname !== 'localhost' && hostname !== '127.0.0.1') { defaultValue: 'http://localhost:3000',
useHttps = await p.confirm({ });
message: 'Is HTTPS enabled for the web UI?',
initialValue: true,
});
}
corsOrigin = deriveCorsOrigin(hostname, 3000, useHttps);
p.log(`CORS origin set to: ${corsOrigin}`);
} }
const authSecret = preservedAuthSecret ?? randomBytes(32).toString('hex'); const authSecret = preservedAuthSecret ?? randomBytes(32).toString('hex');
@@ -527,11 +459,7 @@ async function collectAndWriteConfig(
} }
if (anthropicKey) { if (anthropicKey) {
if (opts.providerType === 'openai') { envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
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 });
@@ -565,7 +493,6 @@ async function collectAndWriteConfig(
valkeyUrl, valkeyUrl,
anthropicKey: anthropicKey || undefined, anthropicKey: anthropicKey || undefined,
corsOrigin, corsOrigin,
hostname,
regeneratedConfig: true, regeneratedConfig: true,
}; };
} }

View File

@@ -1,118 +0,0 @@
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 });
});
});

View File

@@ -1,54 +0,0 @@
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`.');
}
}

View File

@@ -1,98 +0,0 @@
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;
}
}
}

View File

@@ -7,7 +7,7 @@ export async function welcomeStage(p: WizardPrompter, _state: WizardState): Prom
p.note( p.note(
`Mosaic is an agent framework that gives AI coding assistants\n` + `Mosaic is an agent framework that gives AI coding assistants\n` +
`a persistent identity, shared skills, and structured workflows.\n\n` + `a persistent identity, shared skills, and structured workflows.\n\n` +
`It works with Claude Code, Codex, OpenCode, and Pi SDK.\n\n` + `It works with Claude Code, Codex, and OpenCode.\n\n` +
`All config is stored locally in ~/.config/mosaic/.\n` + `All config is stored locally in ~/.config/mosaic/.\n` +
`No data is sent anywhere. No accounts required.`, `No data is sent anywhere. No accounts required.`,
'What is Mosaic?', 'What is Mosaic?',

View File

@@ -1,118 +0,0 @@
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();
}
});
});

View File

@@ -3,19 +3,6 @@ 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;
@@ -75,11 +62,6 @@ export interface GatewayState {
valkeyUrl?: string; valkeyUrl?: string;
anthropicKey?: string; anthropicKey?: string;
corsOrigin: string; corsOrigin: string;
/**
* Raw hostname the user entered (e.g. "localhost", "myserver.example.com").
* The full CORS origin (`corsOrigin`) is derived from this + protocol + webUiPort.
*/
hostname?: string;
/** True when .env + mosaic.config.json were (re)generated in this run. */ /** True when .env + mosaic.config.json were (re)generated in this run. */
regeneratedConfig?: boolean; regeneratedConfig?: boolean;
admin?: GatewayAdminState; admin?: GatewayAdminState;
@@ -99,12 +81,4 @@ 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;
} }

View File

@@ -1,8 +1,9 @@
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 { MenuSection, WizardState } from './types.js'; import type { 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';
@@ -12,10 +13,6 @@ 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;
@@ -57,7 +54,6 @@ 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)
@@ -94,362 +90,72 @@ 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);
// ── Headless bypass ──────────────────────────────────────────────────────── // Stage 3: Quick Start vs Advanced (skip if keeping existing)
// When MOSAIC_ASSUME_YES=1 or no TTY, run the linear headless path.
// This preserves full backward compatibility with tools/install.sh --yes.
const headlessRun = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
if (headlessRun) {
await runHeadlessPath(prompter, state, configService, options);
return;
}
// ── Interactive: Main Menu ─────────────────────────────────────────────────
if (state.installAction === 'fresh' || state.installAction === 'reset') { if (state.installAction === 'fresh' || state.installAction === 'reset') {
await runMenuLoop(prompter, state, configService, options); await modeSelectStage(prompter, state);
} else if (state.installAction === 'reconfigure') { } else if (state.installAction === 'reconfigure') {
state.mode = 'advanced'; 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 userSetupStage(prompter, state);
await toolsSetupStage(prompter, state);
await runtimeSetupStage(prompter, state);
await hooksPreviewStage(prompter, state);
}
// ── Finish & Apply ──────────────────────────────────────────────────────────
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 // Stage 4: SOUL.md
if (!state.completedSections?.has('skills')) {
await skillsSelectStage(prompter, state);
}
// Finalize (writes configs, links runtime assets, syncs skills)
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) {
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); await soulSetupStage(prompter, state);
// USER.md // Stage 5: USER.md
await userSetupStage(prompter, state); await userSetupStage(prompter, state);
// TOOLS.md // Stage 6: TOOLS.md
await toolsSetupStage(prompter, state); await toolsSetupStage(prompter, state);
// Runtime Detection // Stage 7: Runtime Detection & Installation
await runtimeSetupStage(prompter, state); await runtimeSetupStage(prompter, state);
// Hooks // Stage 8: Hooks preview (Claude only — skipped if Claude not detected)
await hooksPreviewStage(prompter, state); await hooksPreviewStage(prompter, state);
// Skills // Stage 9: Skills Selection
await skillsSelectStage(prompter, state); await skillsSelectStage(prompter, state);
// Finalize // Stage 10: Finalize (writes configs, links runtime assets, runs doctor)
await finalizeStage(prompter, state, configService); await finalizeStage(prompter, state, configService);
// Gateway stages // Stages 11 & 12: Gateway config + admin bootstrap.
// 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) { if (!configResult.ready || !configResult.host || !configResult.port) {
prompter.warn('Gateway configuration failed in headless mode — aborting wizard.'); if (headlessRun) {
process.exit(1); prompter.warn('Gateway configuration failed in headless mode — aborting wizard.');
process.exit(1);
}
} else { } else {
const bootstrapResult = await gatewayBootstrapStage(prompter, state, { const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
host: configResult.host, host: configResult.host,
port: configResult.port, port: configResult.port,
}); });
if (!bootstrapResult.completed) { if (!bootstrapResult.completed && headlessRun) {
prompter.warn('Admin bootstrap failed — aborting wizard.'); prompter.warn('Admin bootstrap failed in headless mode — aborting wizard.');
process.exit(1);
}
}
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
}
}
// ── 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); process.exit(1);
} }
} }
} catch (err) { } catch (err) {
// Stages normally return structured `ready: false` results for
// expected failures. Anything that reaches here is an unexpected
// 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.
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;
} }

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/prdy" "directory": "packages/prdy"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.3", "version": "0.0.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/quality-rails" "directory": "packages/quality-rails"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.4", "version": "0.0.4",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/queue" "directory": "packages/queue"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.4", "version": "0.0.4",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/storage" "directory": "packages/storage"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "packages/types" "directory": "packages/types"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "plugins/discord" "directory": "plugins/discord"
}, },
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "plugins/macp" "directory": "plugins/macp"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "plugins/mosaic-framework" "directory": "plugins/mosaic-framework"
}, },
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
"version": "0.0.2", "version": "0.0.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "plugins/telegram" "directory": "plugins/telegram"
}, },
"main": "dist/index.js", "main": "dist/index.js",

377
pnpm-lock.yaml generated
View File

@@ -174,39 +174,21 @@ importers:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.3.6 version: 4.3.6
devDependencies: devDependencies:
'@nestjs/testing':
specifier: ^11.1.18
version: 11.1.18(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)
'@swc/core':
specifier: ^1.15.24
version: 1.15.24(@swc/helpers@0.5.21)
'@swc/helpers':
specifier: ^0.5.21
version: 0.5.21
'@types/node': '@types/node':
specifier: ^22.0.0 specifier: ^22.0.0
version: 22.19.15 version: 22.19.15
'@types/node-cron': '@types/node-cron':
specifier: ^3.0.11 specifier: ^3.0.11
version: 3.0.11 version: 3.0.11
'@types/supertest':
specifier: ^7.2.0
version: 7.2.0
'@types/uuid': '@types/uuid':
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0 version: 10.0.0
supertest:
specifier: ^7.2.2
version: 7.2.2
tsx: tsx:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.21.0 version: 4.21.0
typescript: typescript:
specifier: ^5.8.0 specifier: ^5.8.0
version: 5.9.3 version: 5.9.3
unplugin-swc:
specifier: ^1.5.9
version: 1.5.9(@swc/core@1.15.24(@swc/helpers@0.5.21))(rollup@4.59.0)
vitest: vitest:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1) version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)
@@ -2327,19 +2309,6 @@ packages:
'@nestjs/websockets': ^11.0.0 '@nestjs/websockets': ^11.0.0
rxjs: ^7.1.0 rxjs: ^7.1.0
'@nestjs/testing@11.1.18':
resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==}
peerDependencies:
'@nestjs/common': ^11.0.0
'@nestjs/core': ^11.0.0
'@nestjs/microservices': ^11.0.0
'@nestjs/platform-express': ^11.0.0
peerDependenciesMeta:
'@nestjs/microservices':
optional: true
'@nestjs/platform-express':
optional: true
'@nestjs/throttler@6.5.0': '@nestjs/throttler@6.5.0':
resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==}
peerDependencies: peerDependencies:
@@ -2414,10 +2383,6 @@ packages:
resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
'@noble/hashes@1.8.0':
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@noble/hashes@2.0.1': '@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
@@ -3042,9 +3007,6 @@ packages:
peerDependencies: peerDependencies:
'@opentelemetry/api': ^1.1.0 '@opentelemetry/api': ^1.1.0
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@pinojs/redact@0.4.0': '@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
@@ -3087,15 +3049,6 @@ packages:
'@protobufjs/utf8@1.1.0': '@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.59.0': '@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm] cpu: [arm]
@@ -3437,99 +3390,9 @@ packages:
'@standard-schema/spec@1.1.0': '@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@swc/core-darwin-arm64@1.15.24':
resolution: {integrity: sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
'@swc/core-darwin-x64@1.15.24':
resolution: {integrity: sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
'@swc/core-linux-arm-gnueabihf@1.15.24':
resolution: {integrity: sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
'@swc/core-linux-arm64-gnu@1.15.24':
resolution: {integrity: sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
'@swc/core-linux-arm64-musl@1.15.24':
resolution: {integrity: sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
'@swc/core-linux-ppc64-gnu@1.15.24':
resolution: {integrity: sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==}
engines: {node: '>=10'}
cpu: [ppc64]
os: [linux]
'@swc/core-linux-s390x-gnu@1.15.24':
resolution: {integrity: sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==}
engines: {node: '>=10'}
cpu: [s390x]
os: [linux]
'@swc/core-linux-x64-gnu@1.15.24':
resolution: {integrity: sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
'@swc/core-linux-x64-musl@1.15.24':
resolution: {integrity: sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
'@swc/core-win32-arm64-msvc@1.15.24':
resolution: {integrity: sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
'@swc/core-win32-ia32-msvc@1.15.24':
resolution: {integrity: sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
'@swc/core-win32-x64-msvc@1.15.24':
resolution: {integrity: sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
'@swc/core@1.15.24':
resolution: {integrity: sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==}
engines: {node: '>=10'}
peerDependencies:
'@swc/helpers': '>=0.5.17'
peerDependenciesMeta:
'@swc/helpers':
optional: true
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@swc/helpers@0.5.21':
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
'@swc/types@0.1.26':
resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==}
'@tailwindcss/node@4.2.1': '@tailwindcss/node@4.2.1':
resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==}
@@ -3643,9 +3506,6 @@ packages:
'@types/connect@3.4.38': '@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cookiejar@2.1.5':
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
'@types/cors@2.8.19': '@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
@@ -3676,9 +3536,6 @@ packages:
'@types/memcached@2.2.10': '@types/memcached@2.2.10':
resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==}
'@types/methods@1.1.4':
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
'@types/mime-types@2.1.4': '@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
@@ -3723,12 +3580,6 @@ packages:
'@types/retry@0.12.0': '@types/retry@0.12.0':
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
'@types/superagent@8.1.9':
resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==}
'@types/supertest@7.2.0':
resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==}
'@types/tedious@4.0.14': '@types/tedious@4.0.14':
resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==}
@@ -3937,9 +3788,6 @@ packages:
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
assertion-error@2.0.1: assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -4281,9 +4129,6 @@ packages:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'} engines: {node: '>=20'}
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -4315,9 +4160,6 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cookiejar@2.1.4:
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
core-util-is@1.0.3: core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -4426,9 +4268,6 @@ packages:
devlop@1.1.0: devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
dezalgo@1.0.4:
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
diff@8.0.3: diff@8.0.3:
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
engines: {node: '>=0.3.1'} engines: {node: '>=0.3.1'}
@@ -4734,9 +4573,6 @@ packages:
estree-util-is-identifier-name@3.0.0: estree-util-is-identifier-name@3.0.0:
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3: estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
@@ -4915,10 +4751,6 @@ packages:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
formidable@3.5.4:
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
engines: {node: '>=14.0.0'}
forwarded-parse@2.1.2: forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
@@ -5492,10 +5324,6 @@ packages:
resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==}
engines: {node: '>=13.2.0'} engines: {node: '>=13.2.0'}
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -5634,10 +5462,6 @@ packages:
merge-stream@2.0.0: merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
micromark-core-commonmark@2.0.3: micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -5721,11 +5545,6 @@ packages:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'} engines: {node: '>=18'}
mime@2.6.0:
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
engines: {node: '>=4.0.0'}
hasBin: true
mimic-fn@2.1.0: mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -6683,14 +6502,6 @@ packages:
babel-plugin-macros: babel-plugin-macros:
optional: true optional: true
superagent@10.3.0:
resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==}
engines: {node: '>=14.18.0'}
supertest@7.2.2:
resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==}
engines: {node: '>=14.18.0'}
supports-color@7.2.0: supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -6947,15 +6758,6 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
unplugin-swc@1.5.9:
resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==}
peerDependencies:
'@swc/core': ^1.2.108
unplugin@2.3.11:
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
engines: {node: '>=18.12.0'}
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -7068,9 +6870,6 @@ packages:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
whatwg-mimetype@5.0.0: whatwg-mimetype@5.0.0:
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -8963,12 +8762,6 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@nestjs/testing@11.1.18(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)':
dependencies:
'@nestjs/common': 11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
tslib: 2.8.1
'@nestjs/throttler@6.5.0(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(reflect-metadata@0.2.2)': '@nestjs/throttler@6.5.0(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(reflect-metadata@0.2.2)':
dependencies: dependencies:
'@nestjs/common': 11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -9015,8 +8808,6 @@ snapshots:
'@noble/ciphers@2.1.1': {} '@noble/ciphers@2.1.1': {}
'@noble/hashes@1.8.0': {}
'@noble/hashes@2.0.1': {} '@noble/hashes@2.0.1': {}
'@nuxt/opencollective@0.4.1': '@nuxt/opencollective@0.4.1':
@@ -9931,10 +9722,6 @@ snapshots:
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
'@paralleldrive/cuid2@2.3.1':
dependencies:
'@noble/hashes': 1.8.0
'@pinojs/redact@0.4.0': {} '@pinojs/redact@0.4.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
@@ -9967,14 +9754,6 @@ snapshots:
'@protobufjs/utf8@1.1.0': {} '@protobufjs/utf8@1.1.0': {}
'@rollup/pluginutils@5.3.0(rollup@4.59.0)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 4.59.0
'@rollup/rollup-android-arm-eabi@4.59.0': '@rollup/rollup-android-arm-eabi@4.59.0':
optional: true optional: true
@@ -10372,75 +10151,10 @@ snapshots:
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
'@swc/core-darwin-arm64@1.15.24':
optional: true
'@swc/core-darwin-x64@1.15.24':
optional: true
'@swc/core-linux-arm-gnueabihf@1.15.24':
optional: true
'@swc/core-linux-arm64-gnu@1.15.24':
optional: true
'@swc/core-linux-arm64-musl@1.15.24':
optional: true
'@swc/core-linux-ppc64-gnu@1.15.24':
optional: true
'@swc/core-linux-s390x-gnu@1.15.24':
optional: true
'@swc/core-linux-x64-gnu@1.15.24':
optional: true
'@swc/core-linux-x64-musl@1.15.24':
optional: true
'@swc/core-win32-arm64-msvc@1.15.24':
optional: true
'@swc/core-win32-ia32-msvc@1.15.24':
optional: true
'@swc/core-win32-x64-msvc@1.15.24':
optional: true
'@swc/core@1.15.24(@swc/helpers@0.5.21)':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.26
optionalDependencies:
'@swc/core-darwin-arm64': 1.15.24
'@swc/core-darwin-x64': 1.15.24
'@swc/core-linux-arm-gnueabihf': 1.15.24
'@swc/core-linux-arm64-gnu': 1.15.24
'@swc/core-linux-arm64-musl': 1.15.24
'@swc/core-linux-ppc64-gnu': 1.15.24
'@swc/core-linux-s390x-gnu': 1.15.24
'@swc/core-linux-x64-gnu': 1.15.24
'@swc/core-linux-x64-musl': 1.15.24
'@swc/core-win32-arm64-msvc': 1.15.24
'@swc/core-win32-ia32-msvc': 1.15.24
'@swc/core-win32-x64-msvc': 1.15.24
'@swc/helpers': 0.5.21
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@swc/helpers@0.5.21':
dependencies:
tslib: 2.8.1
'@swc/types@0.1.26':
dependencies:
'@swc/counter': 0.1.3
'@tailwindcss/node@4.2.1': '@tailwindcss/node@4.2.1':
dependencies: dependencies:
'@jridgewell/remapping': 2.3.5 '@jridgewell/remapping': 2.3.5
@@ -10538,8 +10252,6 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.19.15 '@types/node': 22.19.15
'@types/cookiejar@2.1.5': {}
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 22.19.15 '@types/node': 22.19.15
@@ -10572,8 +10284,6 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.19.15 '@types/node': 22.19.15
'@types/methods@1.1.4': {}
'@types/mime-types@2.1.4': {} '@types/mime-types@2.1.4': {}
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
@@ -10623,18 +10333,6 @@ snapshots:
'@types/retry@0.12.0': {} '@types/retry@0.12.0': {}
'@types/superagent@8.1.9':
dependencies:
'@types/cookiejar': 2.1.5
'@types/methods': 1.1.4
'@types/node': 22.19.15
form-data: 4.0.5
'@types/supertest@7.2.0':
dependencies:
'@types/methods': 1.1.4
'@types/superagent': 8.1.9
'@types/tedious@4.0.14': '@types/tedious@4.0.14':
dependencies: dependencies:
'@types/node': 22.19.15 '@types/node': 22.19.15
@@ -10889,15 +10587,14 @@ snapshots:
argparse@2.0.1: {} argparse@2.0.1: {}
asap@2.0.6: {}
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
ast-types@0.13.4: ast-types@0.13.4:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
asynckit@0.4.0: {} asynckit@0.4.0:
optional: true
atomic-sleep@1.0.0: {} atomic-sleep@1.0.0: {}
@@ -11194,6 +10891,7 @@ snapshots:
combined-stream@1.0.8: combined-stream@1.0.8:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
optional: true
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
@@ -11201,8 +10899,6 @@ snapshots:
commander@14.0.3: {} commander@14.0.3: {}
component-emitter@1.3.1: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
consola@3.4.2: {} consola@3.4.2: {}
@@ -11219,8 +10915,6 @@ snapshots:
cookie@1.1.1: {} cookie@1.1.1: {}
cookiejar@2.1.4: {}
core-util-is@1.0.3: {} core-util-is@1.0.3: {}
cors@2.8.6: cors@2.8.6:
@@ -11300,7 +10994,8 @@ snapshots:
escodegen: 2.1.0 escodegen: 2.1.0
esprima: 4.0.1 esprima: 4.0.1
delayed-stream@1.0.0: {} delayed-stream@1.0.0:
optional: true
denque@2.1.0: {} denque@2.1.0: {}
@@ -11314,11 +11009,6 @@ snapshots:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
dezalgo@1.0.4:
dependencies:
asap: 2.0.6
wrappy: 1.0.2
diff@8.0.3: {} diff@8.0.3: {}
discord-api-types@0.38.42: {} discord-api-types@0.38.42: {}
@@ -11470,6 +11160,7 @@ snapshots:
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
optional: true
es-toolkit@1.45.1: {} es-toolkit@1.45.1: {}
@@ -11677,8 +11368,6 @@ snapshots:
estree-util-is-identifier-name@3.0.0: {} estree-util-is-identifier-name@3.0.0: {}
estree-walker@2.0.2: {}
estree-walker@3.0.3: estree-walker@3.0.3:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -11929,17 +11618,12 @@ snapshots:
es-set-tostringtag: 2.1.0 es-set-tostringtag: 2.1.0
hasown: 2.0.2 hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
optional: true
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
dependencies: dependencies:
fetch-blob: 3.2.0 fetch-blob: 3.2.0
formidable@3.5.4:
dependencies:
'@paralleldrive/cuid2': 2.3.1
dezalgo: 1.0.4
once: 1.4.0
forwarded-parse@2.1.2: {} forwarded-parse@2.1.2: {}
forwarded@0.2.0: {} forwarded@0.2.0: {}
@@ -12112,6 +11796,7 @@ snapshots:
has-tostringtag@1.0.2: has-tostringtag@1.0.2:
dependencies: dependencies:
has-symbols: 1.1.0 has-symbols: 1.1.0
optional: true
hasown@2.0.2: hasown@2.0.2:
dependencies: dependencies:
@@ -12583,8 +12268,6 @@ snapshots:
load-esm@1.0.3: {} load-esm@1.0.3: {}
load-tsconfig@0.2.5: {}
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
@@ -12783,8 +12466,6 @@ snapshots:
merge-stream@2.0.0: {} merge-stream@2.0.0: {}
methods@1.1.2: {}
micromark-core-commonmark@2.0.3: micromark-core-commonmark@2.0.3:
dependencies: dependencies:
decode-named-character-reference: 1.3.0 decode-named-character-reference: 1.3.0
@@ -12935,8 +12616,6 @@ snapshots:
dependencies: dependencies:
mime-db: 1.54.0 mime-db: 1.54.0
mime@2.6.0: {}
mimic-fn@2.1.0: {} mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {} mimic-fn@4.0.0: {}
@@ -14017,28 +13696,6 @@ snapshots:
client-only: 0.0.1 client-only: 0.0.1
react: 19.2.4 react: 19.2.4
superagent@10.3.0:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.3
fast-safe-stringify: 2.1.1
form-data: 4.0.5
formidable: 3.5.4
methods: 1.1.2
mime: 2.6.0
qs: 6.15.0
transitivePeerDependencies:
- supports-color
supertest@7.2.2:
dependencies:
cookie-signature: 1.2.2
methods: 1.1.2
superagent: 10.3.0
transitivePeerDependencies:
- supports-color
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
@@ -14294,22 +13951,6 @@ snapshots:
unpipe@1.0.0: {} unpipe@1.0.0: {}
unplugin-swc@1.5.9(@swc/core@1.15.24(@swc/helpers@0.5.21))(rollup@4.59.0):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
'@swc/core': 1.15.24(@swc/helpers@0.5.21)
load-tsconfig: 0.2.5
unplugin: 2.3.11
transitivePeerDependencies:
- rollup
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
acorn: 8.16.0
picomatch: 4.0.3
webpack-virtual-modules: 0.6.2
uri-js@4.4.1: uri-js@4.4.1:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
@@ -14476,8 +14117,6 @@ snapshots:
webidl-conversions@8.0.1: {} webidl-conversions@8.0.1: {}
webpack-virtual-modules@0.6.2: {}
whatwg-mimetype@5.0.0: {} whatwg-mimetype@5.0.0: {}
whatwg-url@14.2.0: whatwg-url@14.2.0:

View File

@@ -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)
# #
# Quick: curl -fsSL https://mosaicstack.dev/install.sh | bash # Remote install (recommended):
# Direct: bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh) # bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-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/stack/raw/branch/main/tools/install.sh | bash -s -- # curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-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/stack" REPO_BASE="https://git.mosaicstack.dev/mosaicstack/mosaic-stack"
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz" ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
# ─── uninstall path ─────────────────────────────────────────────────────────── # ─── uninstall path ───────────────────────────────────────────────────────────