fix(wizard): resolve skills sync script path #690
34
docs/scratchpads/B2-skills-sync-path.md
Normal file
34
docs/scratchpads/B2-skills-sync-path.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# B2 — Fresh-install skills sync path
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Greenfield wizard on `next` reported:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Skills sync script not found at ~/.config/mosaic/bin/mosaic-sync-skills
|
||||||
|
Skills: install failed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Diagnosis
|
||||||
|
|
||||||
|
The framework install migration removed the legacy `~/.config/mosaic/bin/` directory and now installs framework helper scripts under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/mosaic/tools/_scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
`packages/mosaic/src/stages/finalize.ts` still resolved wizard helper scripts from `mosaicHome/bin`, so wizard-selected skills failed even though `mosaic-sync-skills` was present in the current framework layout.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
- Resolve framework helper scripts through `tools/_scripts/<name>` first.
|
||||||
|
- Keep a legacy `bin/<name>` fallback for pre-migration installs.
|
||||||
|
- Point missing-script warnings at the current `tools/_scripts` layout.
|
||||||
|
- Update the finalize skills test fixture to model the fresh framework layout.
|
||||||
|
- Update framework README examples from legacy `bin/` helper paths to `tools/_scripts/`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Unit: `pnpm --filter @mosaicstack/mosaic test -- finalize-skills`
|
||||||
|
- Gates: `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm build`
|
||||||
|
- Fresh path: ran `packages/mosaic/framework/install.sh` with a temp `MOSAIC_HOME` and `MOSAIC_SYNC_ONLY=1`; verified `tools/_scripts/mosaic-sync-skills` exists, legacy `bin/mosaic-sync-skills` does not, and the script installs a selected fake `lint` skill into Mosaic + Pi runtime skill directories.
|
||||||
@@ -118,8 +118,8 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
|||||||
├── TOOLS.md ← Machine-level tool reference (generated by mosaic init)
|
├── TOOLS.md ← Machine-level tool reference (generated by mosaic init)
|
||||||
├── STANDARDS.md ← Machine-wide standards
|
├── STANDARDS.md ← Machine-wide standards
|
||||||
├── guides/ ← Operational guides (E2E delivery, PRD, docs, etc.)
|
├── guides/ ← Operational guides (E2E delivery, PRD, docs, etc.)
|
||||||
├── bin/ ← CLI tools (mosaic launcher, mosaic-init, mosaic-doctor, etc.)
|
|
||||||
├── tools/ ← Tool suites: git, orchestrator, prdy, quality, etc.
|
├── tools/ ← Tool suites: git, orchestrator, prdy, quality, etc.
|
||||||
|
│ └── _scripts/ ← Framework helper scripts (sync skills, doctor, runtime links)
|
||||||
├── runtime/ ← Runtime adapters + runtime-specific references
|
├── runtime/ ← Runtime adapters + runtime-specific references
|
||||||
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
|
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
|
||||||
│ ├── codex/ ← instructions.md, RUNTIME.md
|
│ ├── codex/ ← instructions.md, RUNTIME.md
|
||||||
@@ -194,15 +194,15 @@ bash tools/install.sh --ref v1.0 # Install from a specific git ref (--ref win
|
|||||||
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories.
|
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mosaic sync # Full sync (clone + link)
|
mosaic sync # Full sync (clone + link)
|
||||||
~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only
|
~/.config/mosaic/tools/_scripts/mosaic-sync-skills --link-only # Re-link only
|
||||||
```
|
```
|
||||||
|
|
||||||
## Health Audit
|
## Health Audit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mosaic doctor # Standard audit
|
mosaic doctor # Standard audit
|
||||||
~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode
|
~/.config/mosaic/tools/_scripts/mosaic-doctor --fail-on-warn # Strict mode
|
||||||
```
|
```
|
||||||
|
|
||||||
## MCP Registration
|
## MCP Registration
|
||||||
@@ -213,8 +213,8 @@ sequential-thinking MCP is required for Mosaic Stack. The installer registers it
|
|||||||
To verify or re-register manually:
|
To verify or re-register manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking
|
~/.config/mosaic/tools/_scripts/mosaic-ensure-sequential-thinking
|
||||||
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
|
~/.config/mosaic/tools/_scripts/mosaic-ensure-sequential-thinking --check
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Code MCP Registration
|
### Claude Code MCP Registration
|
||||||
|
|||||||
@@ -85,16 +85,16 @@ function makeConfigService(): ConfigService {
|
|||||||
|
|
||||||
describe('finalizeStage — skill installer', () => {
|
describe('finalizeStage — skill installer', () => {
|
||||||
let tmp: string;
|
let tmp: string;
|
||||||
let binDir: string;
|
let scriptsDir: string;
|
||||||
let syncScript: string;
|
let syncScript: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tmp = mkdtempSync(join(tmpdir(), 'mosaic-finalize-'));
|
tmp = mkdtempSync(join(tmpdir(), 'mosaic-finalize-'));
|
||||||
binDir = join(tmp, 'bin');
|
scriptsDir = join(tmp, 'tools', '_scripts');
|
||||||
mkdirSync(binDir, { recursive: true });
|
mkdirSync(scriptsDir, { recursive: true });
|
||||||
syncScript = join(binDir, 'mosaic-sync-skills');
|
syncScript = join(scriptsDir, 'mosaic-sync-skills');
|
||||||
|
|
||||||
// Default: script exists and succeeds
|
// Default: current framework layout has tools/_scripts and succeeds.
|
||||||
writeFileSync(syncScript, '#!/usr/bin/env bash\necho ok\n', { mode: 0o755 });
|
writeFileSync(syncScript, '#!/usr/bin/env bash\necho ok\n', { mode: 0o755 });
|
||||||
spawnSyncMock.mockReturnValue({ status: 0, stdout: 'ok', stderr: '' });
|
spawnSyncMock.mockReturnValue({ status: 0, stdout: 'ok', stderr: '' });
|
||||||
});
|
});
|
||||||
@@ -122,10 +122,29 @@ describe('finalizeStage — skill installer', () => {
|
|||||||
|
|
||||||
const call = findSkillsSyncCall();
|
const call = findSkillsSyncCall();
|
||||||
expect(call).toBeDefined();
|
expect(call).toBeDefined();
|
||||||
|
expect(call![1]).toEqual([join(tmp, 'tools', '_scripts', 'mosaic-sync-skills')]);
|
||||||
const opts = call![2] as { env?: Record<string, string> };
|
const opts = call![2] as { env?: Record<string, string> };
|
||||||
expect(opts.env?.['MOSAIC_INSTALL_SKILLS']).toBe('brainstorming:lint:systematic-debugging');
|
expect(opts.env?.['MOSAIC_INSTALL_SKILLS']).toBe('brainstorming:lint:systematic-debugging');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('falls back to legacy bin path for pre-migration installs', async () => {
|
||||||
|
rmSync(syncScript);
|
||||||
|
const legacyBinDir = join(tmp, 'bin');
|
||||||
|
mkdirSync(legacyBinDir, { recursive: true });
|
||||||
|
const legacySyncScript = join(legacyBinDir, 'mosaic-sync-skills');
|
||||||
|
writeFileSync(legacySyncScript, '#!/usr/bin/env bash\necho ok\n', { mode: 0o755 });
|
||||||
|
|
||||||
|
const state = makeState(tmp, ['brainstorming']);
|
||||||
|
const p = buildPrompter();
|
||||||
|
const config = makeConfigService();
|
||||||
|
|
||||||
|
await finalizeStage(p, state, config);
|
||||||
|
|
||||||
|
const call = findSkillsSyncCall();
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
expect(call![1]).toEqual([legacySyncScript]);
|
||||||
|
});
|
||||||
|
|
||||||
it('skips the sync script entirely when no skills are selected', async () => {
|
it('skips the sync script entirely when no skills are selected', async () => {
|
||||||
const state = makeState(tmp, []);
|
const state = makeState(tmp, []);
|
||||||
const p = buildPrompter();
|
const p = buildPrompter();
|
||||||
@@ -165,7 +184,9 @@ describe('finalizeStage — skill installer', () => {
|
|||||||
|
|
||||||
// spawnSync should NOT have been called for the skills script
|
// spawnSync should NOT have been called for the skills script
|
||||||
expect(findSkillsSyncCall()).toBeUndefined();
|
expect(findSkillsSyncCall()).toBeUndefined();
|
||||||
expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
expect(p.warn).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('tools/_scripts/mosaic-sync-skills'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes skills count in the summary when install succeeds', async () => {
|
it('includes skills count in the summary when install succeeds', async () => {
|
||||||
|
|||||||
@@ -7,8 +7,21 @@ import type { ConfigService } from '../config/config-service.js';
|
|||||||
import type { WizardState } from '../types.js';
|
import type { WizardState } from '../types.js';
|
||||||
import { getShellProfilePath } from '../platform/detect.js';
|
import { getShellProfilePath } from '../platform/detect.js';
|
||||||
|
|
||||||
|
function frameworkScriptPath(mosaicHome: string, name: string): string {
|
||||||
|
const currentPath = join(mosaicHome, 'tools', '_scripts', name);
|
||||||
|
if (existsSync(currentPath)) return currentPath;
|
||||||
|
|
||||||
|
// Backward-compatible fallback for pre-migration installs that still have bin/.
|
||||||
|
const legacyPath = join(mosaicHome, 'bin', name);
|
||||||
|
if (existsSync(legacyPath)) return legacyPath;
|
||||||
|
|
||||||
|
// Return the current expected path so user-facing errors point at the layout
|
||||||
|
// installed by packages/mosaic/framework/install.sh.
|
||||||
|
return currentPath;
|
||||||
|
}
|
||||||
|
|
||||||
function linkRuntimeAssets(mosaicHome: string, skipClaudeHooks: boolean): void {
|
function linkRuntimeAssets(mosaicHome: string, skipClaudeHooks: boolean): void {
|
||||||
const script = join(mosaicHome, 'bin', 'mosaic-link-runtime-assets');
|
const script = frameworkScriptPath(mosaicHome, 'mosaic-link-runtime-assets');
|
||||||
if (existsSync(script)) {
|
if (existsSync(script)) {
|
||||||
try {
|
try {
|
||||||
spawnSync('bash', [script], {
|
spawnSync('bash', [script], {
|
||||||
@@ -48,7 +61,7 @@ function syncSkills(mosaicHome: string, selectedSkills: string[]): SyncSkillsRes
|
|||||||
return { success: true, installedCount: 0 };
|
return { success: true, installedCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = join(mosaicHome, 'bin', 'mosaic-sync-skills');
|
const script = frameworkScriptPath(mosaicHome, 'mosaic-sync-skills');
|
||||||
if (!existsSync(script)) {
|
if (!existsSync(script)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -96,7 +109,7 @@ interface DoctorResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runDoctor(mosaicHome: string): DoctorResult {
|
function runDoctor(mosaicHome: string): DoctorResult {
|
||||||
const script = join(mosaicHome, 'bin', 'mosaic-doctor');
|
const script = frameworkScriptPath(mosaicHome, 'mosaic-doctor');
|
||||||
if (!existsSync(script)) {
|
if (!existsSync(script)) {
|
||||||
return { warnings: 0, output: 'mosaic-doctor not found' };
|
return { warnings: 0, output: 'mosaic-doctor not found' };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user