Compare commits

...

11 Commits

Author SHA1 Message Date
Jarvis
1b4767bd8b fix: add build tools to CI install step for better-sqlite3 native bindings
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-04-03 07:41:39 -05:00
0b0fe10b37 Merge pull request 'feat: storage abstraction retrofit — adapters for queue, storage, memory (phases 1-4)' (#365) from feat/storage-abstraction into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 04:40:57 +00:00
acfb31f8f6 fix: quality-rails Commander version mismatch + installer defaults (#364)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 02:40:02 +00:00
d4c5797a65 fix: installer copies default framework files (AGENTS.md) to mosaicHome (#363)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:34:43 +00:00
70a51ba711 fix: all CLI script resolution uses bundled-first resolveTool() (#362)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:28:07 +00:00
db8023bdbb fix: fwScript prefers npm-bundled scripts over stale deployed copies (#361)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:21:58 +00:00
9e597ecf87 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.6 (#360)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:13:37 +00:00
a23c117ea4 fix: auto-migrate customized skills to skills-local/ on sync (#359)
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-03 02:11:03 +00:00
0cf80dab8c fix: stale update banner + skill sync dirty worktree crash (#358)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:04:05 +00:00
381b0eed7b Merge pull request 'chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.4' (#357) from chore/bump-0.0.4 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#357
2026-04-03 01:51:55 +00:00
Jarvis
e7db9ddf98 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.4
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-02 20:50:44 -05:00
12 changed files with 168 additions and 55 deletions

View File

@@ -15,6 +15,7 @@ steps:
image: *node_image
commands:
- corepack enable
- apk add --no-cache python3 make g++
- pnpm install --frozen-lockfile
typecheck:

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/cli",
"version": "0.0.3",
"version": "0.0.10",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -2,7 +2,7 @@
import { createRequire } from 'module';
import { Command } from 'commander';
import { createQualityRailsCli } from '@mosaic/quality-rails';
import { registerQualityRails } from '@mosaic/quality-rails';
import { registerAgentCommand } from './commands/agent.js';
import { registerMissionCommand } from './commands/mission.js';
// prdy is registered via launch.ts
@@ -305,11 +305,7 @@ registerMissionCommand(program);
// ─── quality-rails ──────────────────────────────────────────────────────
const qrWrapper = createQualityRailsCli();
const qrCmd = qrWrapper.commands.find((c) => c.name() === 'quality-rails');
if (qrCmd !== undefined) {
program.addCommand(qrCmd as unknown as Command);
}
registerQualityRails(program);
// ─── update ─────────────────────────────────────────────────────────────

View File

@@ -7,6 +7,7 @@
import { execFileSync, execSync, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
import { createRequire } from 'node:module';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import type { Command } from 'commander';
@@ -67,7 +68,7 @@ function checkSoul(): void {
}
// Fallback: legacy bash mosaic-init
const initBin = join(MOSAIC_HOME, 'tools', '_scripts', 'mosaic-init');
const initBin = fwScript('mosaic-init');
if (existsSync(initBin)) {
spawnSync(initBin, [], { stdio: 'inherit' });
} else {
@@ -78,7 +79,7 @@ function checkSoul(): void {
}
function checkSequentialThinking(runtime: string): void {
const checker = join(MOSAIC_HOME, 'tools', '_scripts', 'mosaic-ensure-sequential-thinking');
const checker = fwScript('mosaic-ensure-sequential-thinking');
if (!existsSync(checker)) return; // Skip if checker doesn't exist
const result = spawnSync(checker, ['--check', '--runtime', runtime], { stdio: 'ignore' });
if (result.status !== 0) {
@@ -491,12 +492,29 @@ function delegateToScript(scriptPath: string, args: string[], env?: Record<strin
}
}
/**
* Resolve a path under the framework tools directory. Prefers the version
* bundled in the @mosaic/mosaic npm package (always matches the installed
* CLI version) over the deployed copy in ~/.config/mosaic/ (may be stale).
*/
function resolveTool(...segments: string[]): string {
try {
const req = createRequire(import.meta.url);
const mosaicPkg = dirname(req.resolve('@mosaic/mosaic/package.json'));
const bundled = join(mosaicPkg, 'framework', 'tools', ...segments);
if (existsSync(bundled)) return bundled;
} catch {
// Fall through to deployed copy
}
return join(MOSAIC_HOME, 'tools', ...segments);
}
function fwScript(name: string): string {
return join(MOSAIC_HOME, 'tools', '_scripts', name);
return resolveTool('_scripts', name);
}
function toolScript(toolDir: string, name: string): string {
return join(MOSAIC_HOME, 'tools', toolDir, name);
return resolveTool(toolDir, name);
}
// ─── Coord (mission orchestrator) ───────────────────────────────────────────

View File

@@ -56,24 +56,66 @@ if [[ $fetch -eq 1 ]]; then
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
# Stash any local changes (dirty index or worktree) before pulling
local_changes=0
if ! git -C "$SKILLS_REPO_DIR" diff --quiet 2>/dev/null || \
! git -C "$SKILLS_REPO_DIR" diff --cached --quiet 2>/dev/null; then
local_changes=1
echo "[mosaic-skills] Stashing local changes..."
git -C "$SKILLS_REPO_DIR" stash push -q -m "mosaic-sync-skills auto-stash"
# ── Detect dirty state ──────────────────────────────────────────────
dirty=""
dirty="$(git -C "$SKILLS_REPO_DIR" status --porcelain 2>/dev/null || true)"
if [[ -n "$dirty" ]]; then
# ── Auto-migrate customized skills to skills-local/ ─────────────
# Instead of stash/pop (fragile, merge conflicts), we:
# 1. Identify which skill dirs contain user edits
# 2. Copy those full skill dirs into skills-local/ (preserving edits)
# 3. Reset the repo clean so pull always succeeds
# 4. skills-local/ takes precedence during linking, so edits win
SOURCE_SKILLS_SUBDIR="$SKILLS_REPO_DIR/skills"
migrated=()
while IFS= read -r line; do
# porcelain format: XY <path> — extract the file path
file="${line:3}"
# Only migrate files under skills/ subdir in the repo
if [[ "$file" == skills/* ]]; then
# Extract the skill directory name (first path component after skills/)
skill_name="${file#skills/}"
skill_name="${skill_name%%/*}"
# Skip if already migrated this skill in this run
local_skill_dir="$MOSAIC_LOCAL_SKILLS_DIR/$skill_name"
if [[ -d "$local_skill_dir" ]]; then
continue
fi
# Skip if skill_name is empty or hidden
if [[ -z "$skill_name" || "$skill_name" == .* ]]; then
continue
fi
# Copy the skill (with user's edits) from repo working tree to skills-local/
if [[ -d "$SOURCE_SKILLS_SUBDIR/$skill_name" ]]; then
cp -R "$SOURCE_SKILLS_SUBDIR/$skill_name" "$local_skill_dir"
migrated+=("$skill_name")
fi
fi
done <<< "$dirty"
if [[ ${#migrated[@]} -gt 0 ]]; then
echo "[mosaic-skills] Migrated ${#migrated[@]} customized skill(s) to skills-local/:"
for s in "${migrated[@]}"; do
echo " → $MOSAIC_LOCAL_SKILLS_DIR/$s"
done
echo "[mosaic-skills] Your edits are preserved there and take precedence over canonical."
fi
# Reset repo to clean state so pull always works
echo "[mosaic-skills] Resetting source repo to clean state..."
git -C "$SKILLS_REPO_DIR" checkout . 2>/dev/null || true
git -C "$SKILLS_REPO_DIR" clean -fd 2>/dev/null || true
fi
if ! git -C "$SKILLS_REPO_DIR" pull --rebase; then
if ! git -C "$SKILLS_REPO_DIR" pull --rebase 2>/dev/null; then
echo "[mosaic-skills] WARN: pull failed — continuing with existing checkout" >&2
fi
# Restore stashed changes
if [[ $local_changes -eq 1 ]]; then
echo "[mosaic-skills] Restoring local changes..."
git -C "$SKILLS_REPO_DIR" stash pop -q 2>/dev/null || \
echo "[mosaic-skills] WARN: stash pop had conflicts — check $SKILLS_REPO_DIR" >&2
git -C "$SKILLS_REPO_DIR" rebase --abort 2>/dev/null || true
fi
else
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/mosaic",
"version": "0.0.3-1",
"version": "0.0.10",
"description": "Mosaic agent framework — installation wizard and meta package",
"type": "module",
"main": "dist/index.js",

View File

@@ -1,4 +1,4 @@
import { readFileSync, existsSync } from 'node:fs';
import { readFileSync, existsSync, readdirSync, statSync, copyFileSync } from 'node:fs';
import { join } from 'node:path';
import type { ConfigService } from './config-service.js';
import type { SoulConfig, UserConfig, ToolsConfig, InstallAction } from '../types.js';
@@ -140,6 +140,23 @@ export class FileConfigAdapter implements ConfigService {
preserve: preservePaths,
excludeGit: true,
});
// Copy default root-level .md files (AGENTS.md, STANDARDS.md, etc.)
// from framework/defaults/ into mosaicHome root if they don't exist yet.
// These are framework contracts — only written on first install, never
// overwritten (user may have customized them).
const defaultsDir = join(this.sourceDir, 'defaults');
if (existsSync(defaultsDir)) {
for (const entry of readdirSync(defaultsDir)) {
const dest = join(this.mosaicHome, entry);
if (!existsSync(dest)) {
const src = join(defaultsDir, entry);
if (statSync(src).isFile()) {
copyFileSync(src, dest);
}
}
}
}
}
/**

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { resolve } from 'node:path';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Command } from 'commander';
@@ -49,7 +50,14 @@ program
.action(async (opts: Record<string, string | boolean | undefined>) => {
try {
const mosaicHome = (opts['mosaicHome'] as string) ?? DEFAULT_MOSAIC_HOME;
const sourceDir = (opts['sourceDir'] as string | undefined) ?? mosaicHome;
// Default source to the framework/ dir bundled in this npm package.
// This ensures syncFramework copies AGENTS.md, STANDARDS.md, guides/, etc.
// Falls back to mosaicHome if the bundled dir doesn't exist (shouldn't happen).
const pkgRoot = dirname(fileURLToPath(import.meta.url));
const bundledFramework = resolve(pkgRoot, '..', 'framework');
const sourceDir =
(opts['sourceDir'] as string | undefined) ??
(existsSync(bundledFramework) ? bundledFramework : mosaicHome);
const prompter = opts['nonInteractive'] ? new HeadlessPrompter() : new ClackPrompter();

View File

@@ -122,10 +122,18 @@ export function semverLt(a: string, b: string): boolean {
// ─── Cache ──────────────────────────────────────────────────────────────────
function readCache(): UpdateCheckResult | null {
/** Cache stores only the latest registry version (the expensive network call).
* The installed version is always checked fresh — it's a local `npm ls`. */
interface RegistryCache {
latest: string;
checkedAt: string;
registry: string;
}
function readCache(): RegistryCache | null {
try {
if (!existsSync(CACHE_FILE)) return null;
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as UpdateCheckResult;
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as RegistryCache;
const age = Date.now() - new Date(raw.checkedAt).getTime();
if (age > CACHE_TTL_MS) return null;
return raw;
@@ -134,10 +142,10 @@ function readCache(): UpdateCheckResult | null {
}
}
function writeCache(result: UpdateCheckResult): void {
function writeCache(entry: RegistryCache): void {
try {
mkdirSync(CACHE_DIR, { recursive: true });
writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2) + '\n', 'utf-8');
writeFileSync(CACHE_FILE, JSON.stringify(entry, null, 2) + '\n', 'utf-8');
} catch {
// Best-effort — cache is not critical
}
@@ -174,29 +182,40 @@ export function getLatestVersion(): string {
}
/**
* Perform an update check — uses cache when fresh, otherwise hits registry.
* Perform an update check — uses registry cache when fresh, always checks
* installed version fresh (local npm ls is cheap, caching it causes stale
* "update available" banners after an upgrade).
* Never throws.
*/
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
const current = getInstalledVersion();
let latest: string;
let checkedAt: string;
if (!options?.skipCache) {
const cached = readCache();
if (cached) return cached;
if (cached) {
latest = cached.latest;
checkedAt = cached.checkedAt;
} else {
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({ latest, checkedAt, registry: REGISTRY });
}
} else {
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({ latest, checkedAt, registry: REGISTRY });
}
const current = getInstalledVersion();
const latest = getLatestVersion();
const updateAvailable = !!(current && latest && semverLt(current, latest));
const result: UpdateCheckResult = {
return {
current,
latest,
updateAvailable,
checkedAt: new Date().toISOString(),
updateAvailable: !!(current && latest && semverLt(current, latest)),
checkedAt,
registry: REGISTRY,
};
writeCache(result);
return result;
}
/**

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/quality-rails",
"version": "0.0.2",
"version": "0.0.3",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -17,7 +17,7 @@
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"commander": "^12.0.0"
"commander": "^13.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",

View File

@@ -106,12 +106,26 @@ function printScaffoldResult(
}
}
/**
* Register quality-rails subcommands on an existing Commander program.
* This avoids cross-package Commander version mismatches by using the
* caller's Command instance directly.
*/
export function registerQualityRails(parent: Command): void {
buildQualityRailsCommand(
parent.command('quality-rails').description('Manage quality rails scaffolding'),
);
}
export function createQualityRailsCli(): Command {
const program = new Command('mosaic');
const qualityRails = program
.command('quality-rails')
.description('Manage quality rails scaffolding');
buildQualityRailsCommand(
program.command('quality-rails').description('Manage quality rails scaffolding'),
);
return program;
}
function buildQualityRailsCommand(qualityRails: Command): void {
qualityRails
.command('init')
.requiredOption('--project <path>', 'Project path')
@@ -184,8 +198,6 @@ export function createQualityRailsCli(): Command {
);
}
});
return program;
}
export async function runQualityRailsCli(argv: string[] = process.argv): Promise<void> {

4
pnpm-lock.yaml generated
View File

@@ -572,8 +572,8 @@ importers:
packages/quality-rails:
dependencies:
commander:
specifier: ^12.0.0
version: 12.1.0
specifier: ^13.0.0
version: 13.1.0
devDependencies:
'@types/node':
specifier: ^22.0.0