fix: bootstrap DTO class erasure + wizard failure + port prefill + Pi SDK copy (IUV-M01, #436)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

- Bug 1 [CRITICAL]: drop `import type` on BootstrapSetupDto in bootstrap.controller.ts
  so NestJS preserves the class reference for design:paramtypes; ValidationPipe now
  correctly validates the DTO instead of 400ing on every field.
  Add e2e integration test (bootstrap.e2e.spec.ts) via @nestjs/testing + supertest
  that exercises the real DI/Fastify binding path to guard against regressions.
  Configure vitest to use unplugin-swc with decoratorMetadata:true.

- Bug 2: remove `&& headlessRun` guard in wizard.ts so bootstrap failure (completed:false)
  aborts with process.exit(1) in both interactive and headless modes — no more
  silent '✔ Wizard complete' after a 400.

- Bug 3: add `initialValue` to WizardPrompter.text() interface, ClackPrompter, and
  HeadlessPrompter; use it in gateway-config.ts promptPort() so 14242 prefills the
  input buffer and users can press Enter to accept. Apply same pattern to other
  gateway config prompts (databaseUrl, valkeyUrl, corsOrigin).

- Bug 4: add Pi SDK to the 'What is Mosaic?' intro copy in welcome.ts.

- Bump @mosaicstack/mosaic 0.0.25 → 0.0.26.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-04-05 16:38:37 -05:00
parent a8cd52e88c
commit 56dde4126b
13 changed files with 608 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaicstack/mosaic",
"version": "0.0.25",
"version": "0.0.26",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",

View File

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

View File

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

View File

@@ -24,6 +24,8 @@ export interface WizardPrompter {
message: string;
placeholder?: 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;
}): Promise<string>;

View File

@@ -77,6 +77,10 @@ async function promptTier(p: WizardPrompter): Promise<GatewayStorageTier> {
async function promptPort(p: WizardPrompter, defaultPort: number): Promise<number> {
const raw = await p.text({
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(),
validate: (v) => {
const n = parseInt(v, 10);
@@ -423,10 +427,12 @@ async function collectAndWriteConfig(
if (tier === 'team') {
databaseUrl = await p.text({
message: 'DATABASE_URL',
initialValue: 'postgresql://mosaic:mosaic@localhost:5433/mosaic',
defaultValue: 'postgresql://mosaic:mosaic@localhost:5433/mosaic',
});
valkeyUrl = await p.text({
message: 'VALKEY_URL',
initialValue: 'redis://localhost:6380',
defaultValue: 'redis://localhost:6380',
});
}
@@ -438,6 +444,7 @@ async function collectAndWriteConfig(
corsOrigin = await p.text({
message: 'CORS origin',
initialValue: 'http://localhost:3000',
defaultValue: 'http://localhost:3000',
});
}

View File

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

View File

@@ -144,8 +144,8 @@ export async function runWizard(options: WizardOptions): Promise<void> {
host: configResult.host,
port: configResult.port,
});
if (!bootstrapResult.completed && headlessRun) {
prompter.warn('Admin bootstrap failed in headless mode — aborting wizard.');
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
}
}