factory.yaml: new schema (schema_version 1, factory_version 0.3.0) - core.components[] with id/source/version/type/install/health_check - versions from bundled lib/: projects-meta-mcp 2.25.0, wiki-graph 0.3.1, interns-mcp 0.3.3 - modules[]: tasks, wiki, workshop, admin — each with creates[] describing gitea_repo / local_clone / claude_md_snippet / setup_skill actions - removed hardcoded git_host/git_org (those belong in ~/.config/factory/home.toml per design) bootstrap.ps1: add Read-FactoryManifest + manifest-driven MCP build loop - indent-aware YAML parser prevents health_check.type from clobbering component.type - MCP build loop now reads from manifest instead of hardcoded server names - reports factory version at startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
330 lines
12 KiB
PowerShell
330 lines
12 KiB
PowerShell
#!/usr/bin/env pwsh
|
|
# L0b Factory Bootstrap — Windows
|
|
# Auto-generated from install-log.md field-test 2026-05-06
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$PSVersionTable.PSVersion.Major -ge 7 -or (Write-Error "Bootstrap requires PowerShell 7+. Install: winget install Microsoft.PowerShell"; exit 1)
|
|
|
|
$GITEA_DEFAULT = "https://git.kzntsv.site"
|
|
$ORG_DEFAULT = "OpeItcLoc03"
|
|
$FACTORY_REPO_DEFAULT = "factory"
|
|
$HOME_TOML = "$env:USERPROFILE\.config\factory\home.toml"
|
|
$PROJECTS_DIR_DEFAULT = "C:\Users\$env:USERNAME\projects"
|
|
|
|
function Test-Command {
|
|
param([string]$Name)
|
|
$null = Get-Command $Name -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Read-FactoryManifest {
|
|
param([string]$FactoryPath)
|
|
|
|
$yamlPath = "$FactoryPath\factory.yaml"
|
|
if (-not (Test-Path $yamlPath)) {
|
|
Write-Gap "factory.yaml not found at $yamlPath"
|
|
return $null
|
|
}
|
|
|
|
$lines = Get-Content $yamlPath
|
|
$manifest = @{
|
|
schema_version = ""
|
|
factory_version = ""
|
|
core_components = [System.Collections.Generic.List[hashtable]]::new()
|
|
modules = [System.Collections.Generic.List[hashtable]]::new()
|
|
}
|
|
|
|
$section = ""
|
|
$currentItem = $null
|
|
$propPrefix = "" # exact indent of direct item properties (avoids nested key collisions)
|
|
|
|
foreach ($line in $lines) {
|
|
if ($line -match '^\s*#' -or $line -match '^\s*$') { continue }
|
|
|
|
if ($line -match '^schema_version:\s*"([^"]+)"') { $manifest.schema_version = $matches[1]; continue }
|
|
if ($line -match '^factory_version:\s*"([^"]+)"') { $manifest.factory_version = $matches[1]; continue }
|
|
|
|
if ($line -match '^core:') { $section = ""; $currentItem = $null; continue }
|
|
if ($line -match '^\s+components:') { $section = "core_components"; $currentItem = $null; continue }
|
|
if ($line -match '^modules:') { $section = "modules"; $currentItem = $null; continue }
|
|
|
|
# List item start — detect indent to build exact property prefix
|
|
if ($line -match '^(\s+)-\s+id:\s+(\S+)') {
|
|
$itemIndent = $matches[1].Length
|
|
$propPrefix = " " * ($itemIndent + 2)
|
|
$currentItem = @{ id = $matches[2]; type = ""; source = ""; version = ""; description = ""; optional = $true }
|
|
if ($section -eq "core_components") { $manifest.core_components.Add($currentItem) }
|
|
elseif ($section -eq "modules") { $manifest.modules.Add($currentItem) }
|
|
continue
|
|
}
|
|
|
|
if (-not $currentItem) { continue }
|
|
|
|
# Match only at direct-property indent (prevents health_check.type clobbering component.type)
|
|
if ($line -match "^${propPrefix}type:\s+(\S+)") { $currentItem.type = $matches[1]; continue }
|
|
if ($line -match "^${propPrefix}source:\s+(\S+)") { $currentItem.source = $matches[1]; continue }
|
|
if ($line -match "^${propPrefix}version:\s+`"([^`"]+)`"") { $currentItem.version = $matches[1]; continue }
|
|
if ($line -match "^${propPrefix}description:\s+`"([^`"]+)`"") { $currentItem.description = $matches[1]; continue }
|
|
if ($line -match "^${propPrefix}optional:\s+(\S+)") { $currentItem.optional = ($matches[1] -eq "true"); continue }
|
|
}
|
|
|
|
return $manifest
|
|
}
|
|
|
|
function Write-Step {
|
|
param([string]$Msg)
|
|
Write-Host "`n▶ $Msg" -ForegroundColor Cyan
|
|
}
|
|
|
|
function Write-Ok {
|
|
param([string]$Msg)
|
|
Write-Host "✓ $Msg" -ForegroundColor Green
|
|
}
|
|
|
|
function Write-Skip {
|
|
param([string]$Msg)
|
|
Write-Host "⊘ $Msg" -ForegroundColor Yellow
|
|
}
|
|
|
|
function Write-Gap {
|
|
param([string]$Msg)
|
|
Write-Host "⚠ $Msg" -ForegroundColor Red
|
|
}
|
|
|
|
# Pre-checks
|
|
Write-Step "L0b Pre-checks"
|
|
|
|
$missed = @()
|
|
if (-not (Test-Command git)) { $missed += "git (install: winget install Git.Git)" }
|
|
if (-not (Test-Command curl)) { $missed += "curl (install: winget install curl.curl)" }
|
|
if (-not (Test-Command mise)) { $missed += "mise (install: winget install jdx.mise)" }
|
|
if (-not (Test-Command claude)) { $missed += "claude (install: irm https://claude.ai/install.ps1 | iex)" }
|
|
|
|
if ($missed) {
|
|
Write-Gap "Missing dependencies:"
|
|
$missed | ForEach-Object { Write-Host " - $_" }
|
|
Write-Host "`nInstall missing tools and re-run bootstrap."
|
|
exit 1
|
|
}
|
|
Write-Ok "Pre-checks passed"
|
|
|
|
# Check if already bootstrapped
|
|
if (Test-Path $HOME_TOML) {
|
|
Write-Skip "Already bootstrapped. Re-run mode: verify only."
|
|
|
|
$toml = Get-Content $HOME_TOML -Raw
|
|
if ($toml -match 'projects_dir\s*=\s*''([^'']+)''') {
|
|
$existing_dir = $matches[1]
|
|
Write-Host " projects_dir: $existing_dir"
|
|
|
|
if (Test-Path "$existing_dir\.factory") {
|
|
Write-Ok ".factory exists"
|
|
} else {
|
|
Write-Gap ".factory missing at $existing_dir\.factory"
|
|
exit 1
|
|
}
|
|
}
|
|
Write-Ok "Bootstrap verified. Exiting (no-op)."
|
|
exit 0
|
|
}
|
|
|
|
# Interactive: ask for configuration
|
|
Write-Step "Configuration"
|
|
|
|
$gitea = Read-Host "Gitea URL [$GITEA_DEFAULT]"
|
|
$gitea = if ($gitea) { $gitea } else { $GITEA_DEFAULT }
|
|
|
|
$org = Read-Host "Gitea org/user [$ORG_DEFAULT]"
|
|
$org = if ($org) { $org } else { $ORG_DEFAULT }
|
|
|
|
$factoryRepo = Read-Host "Factory repo name [$FACTORY_REPO_DEFAULT]"
|
|
$factoryRepo = if ($factoryRepo) { $factoryRepo } else { $FACTORY_REPO_DEFAULT }
|
|
|
|
$projectsDir = Read-Host "Projects directory [$PROJECTS_DIR_DEFAULT]"
|
|
$projectsDir = if ($projectsDir) { $projectsDir } else { $PROJECTS_DIR_DEFAULT }
|
|
|
|
$client = Read-Host "AI client [claude-code/copilot-cli/gemini-cli] (default: claude-code)"
|
|
$client = if ($client) { $client } else { "claude-code" }
|
|
|
|
# Validate projects_dir
|
|
if (-not (Test-Path $projectsDir)) {
|
|
$create = Read-Host "Directory '$projectsDir' does not exist. Create? [Y/n]"
|
|
if ($create -ne "n") {
|
|
New-Item -ItemType Directory -Force -Path $projectsDir | Out-Null
|
|
Write-Ok "Created $projectsDir"
|
|
} else {
|
|
Write-Gap "Cannot continue without projects_dir"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Clone .factory
|
|
Write-Step "Cloning .factory repo"
|
|
|
|
$factoryLocal = "$projectsDir\.factory"
|
|
$factoryUrl = "$gitea/$org/$factoryRepo.git"
|
|
|
|
if (Test-Path $factoryLocal) {
|
|
Write-Skip ".factory already exists, skipping clone"
|
|
|
|
# Verify it's a git repo
|
|
if (-not (Test-Path "$factoryLocal\.git")) {
|
|
Write-Gap "$factoryLocal exists but is not a git repo. Remove it and re-run."
|
|
exit 1
|
|
}
|
|
} else {
|
|
Write-Host "Cloning from: $factoryUrl"
|
|
|
|
# Check if we have credentials for gitea
|
|
$needAuth = $false
|
|
try {
|
|
$null = git ls-remote "$factoryUrl" 2>&1
|
|
} catch {
|
|
$needAuth = $true
|
|
}
|
|
|
|
if ($needAuth) {
|
|
Write-Host "`n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
|
|
Write-Host " PAT REQUIRED" -ForegroundColor Yellow
|
|
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
|
|
Write-Host "`n1. Open in browser: $gitea/user/settings/applications"
|
|
Write-Host "2. Generate PAT with scopes: read:repository, write:repository, read:user"
|
|
Write-Host "3. Press Enter when ready...`n"
|
|
Read-Host
|
|
|
|
Write-Host "Cloning with Git Credential Manager prompt..."
|
|
}
|
|
|
|
git clone "$factoryUrl" "$factoryLocal"
|
|
|
|
if (-not $?) {
|
|
Write-Gap "Clone failed. Check URL and credentials."
|
|
exit 1
|
|
}
|
|
Write-Ok "Cloned to $factoryLocal"
|
|
}
|
|
|
|
# Write home.toml
|
|
Write-Step "Writing ~/.config/factory/home.toml"
|
|
|
|
$configDir = "$env:USERPROFILE\.config\factory"
|
|
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
|
|
|
$installedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
|
|
$lines = @(
|
|
"# factory home — single source of machine-level state",
|
|
"# auto-generated by L0b bootstrap; safe to edit by hand",
|
|
"",
|
|
"projects_dir = '$projectsDir'",
|
|
"client = '$client'",
|
|
"installed_at = '$installedAt'"
|
|
)
|
|
|
|
[System.IO.File]::WriteAllLines(
|
|
"$configDir\home.toml",
|
|
$lines,
|
|
[System.Text.UTF8Encoding]::new($false)
|
|
)
|
|
|
|
Write-Ok "Wrote $HOME_TOML"
|
|
|
|
# Read factory manifest
|
|
Write-Step "Reading factory manifest"
|
|
|
|
$manifest = Read-FactoryManifest -FactoryPath $factoryLocal
|
|
if (-not $manifest) { exit 1 }
|
|
Write-Ok "factory v$($manifest.factory_version) (schema $($manifest.schema_version))"
|
|
$coreIds = ($manifest.core_components | ForEach-Object { $_.id }) -join ", "
|
|
$moduleIds = ($manifest.modules | ForEach-Object { $_.id }) -join ", "
|
|
Write-Host " Core: $coreIds"
|
|
Write-Host " Modules: $moduleIds"
|
|
|
|
# Build MCP servers from lib/ (driven by manifest core.components)
|
|
Write-Step "Building MCP servers"
|
|
|
|
$libPath = "$factoryLocal\lib"
|
|
|
|
if (Test-Path $libPath) {
|
|
foreach ($component in $manifest.core_components) {
|
|
$srcPath = "$factoryLocal\$($component.source)"
|
|
if (-not (Test-Path $srcPath)) { continue }
|
|
|
|
if ($component.type -eq "typescript") {
|
|
Write-Host " Building $($component.id) v$($component.version)..."
|
|
Push-Location $srcPath
|
|
try {
|
|
npm install --silent 2>&1 | Out-Null
|
|
npm run build 2>&1 | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Gap "$($component.id): build failed (npm run build exited $LASTEXITCODE)"
|
|
} else {
|
|
Write-Ok "$($component.id) v$($component.version) built"
|
|
}
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
} elseif ($component.type -eq "python") {
|
|
Write-Host " Setting up $($component.id) v$($component.version)..."
|
|
Push-Location $srcPath
|
|
try {
|
|
python -m venv .venv 2>&1 | Out-Null
|
|
& ".venv\Scripts\pip.exe" install -e . --quiet
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Gap "$($component.id): pip install failed"
|
|
} else {
|
|
Write-Ok "$($component.id) v$($component.version) installed"
|
|
}
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Write-Skip "lib/ not found — skipping MCP server build"
|
|
}
|
|
|
|
# Install skills to ~/.claude/skills/
|
|
Write-Step "Installing skills to ~/.claude/skills/"
|
|
|
|
$skillsPath = "$factoryLocal\skills"
|
|
$claudeSkillsPath = "$env:USERPROFILE\.claude\skills"
|
|
|
|
if (Test-Path $skillsPath) {
|
|
New-Item -ItemType Directory -Force -Path $claudeSkillsPath | Out-Null
|
|
Get-ChildItem $skillsPath -Directory | ForEach-Object {
|
|
Copy-Item $_.FullName "$claudeSkillsPath\$($_.Name)" -Recurse -Force
|
|
}
|
|
$count = (Get-ChildItem $skillsPath -Directory).Count
|
|
Write-Ok "Installed $count skill directories to $claudeSkillsPath"
|
|
} else {
|
|
Write-Skip "skills/ not found in factory — skipping skill install"
|
|
}
|
|
|
|
# Print next steps
|
|
Write-Host "`n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Green
|
|
Write-Host " L0b COMPLETE" -ForegroundColor Green
|
|
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Green
|
|
Write-Host "`nFactory bootstrapped. Next steps (manual):`n"
|
|
|
|
Write-Host "1. Start Claude Code:" -ForegroundColor Cyan
|
|
Write-Host " cd $factoryLocal"
|
|
Write-Host " claude`n"
|
|
|
|
Write-Host "2. In Claude, run:" -ForegroundColor Cyan
|
|
Write-Host " /login"
|
|
Write-Host " /plugin install superpowers@claude-plugins-official"
|
|
Write-Host " /plugin install context7@claude-plugins-official"
|
|
Write-Host " /reload-plugins"
|
|
Write-Host " /exit`n"
|
|
|
|
Write-Host "3. Run setup skills:" -ForegroundColor Cyan
|
|
Write-Host " cd $projectsDir"
|
|
Write-Host " claude"
|
|
Write-Host " /setup-projects-meta"
|
|
Write-Host " /setup-interns"
|
|
Write-Host " /setup-tasks"
|
|
Write-Host " /setup-wiki`n"
|
|
|
|
Write-Host "See .wiki/concepts/factory-bootstrap.md for full design."
|
|
Write-Host "`nRe-run this script anytime — it's idempotent."
|