Compare commits
11 Commits
feat/stora
...
fix/pnpm-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1274df7ffc | ||
| 0b0fe10b37 | |||
| acfb31f8f6 | |||
| d4c5797a65 | |||
| 70a51ba711 | |||
| db8023bdbb | |||
| 9e597ecf87 | |||
| a23c117ea4 | |||
| 0cf80dab8c | |||
| 381b0eed7b | |||
|
|
e7db9ddf98 |
@@ -23,5 +23,10 @@
|
||||
"turbo": "^2.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"better-sqlite3"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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) ───────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
if ! git -C "$SKILLS_REPO_DIR" pull --rebase; then
|
||||
# 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 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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
4
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user