aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
780 lines (695 loc) • 24.1 kB
JavaScript
/**
* CLI Workspace Status Tool
*
* Shows workspace health, installed frameworks, and migration status.
*
* @module tools/cli/workspace-status
* @version 1.0.0
* @since 2025-12-09
*
* @usage
* # Show workspace status
* aiwg -status
*
* # Show detailed framework health
* aiwg -status --verbose
*
* # Output as JSON
* aiwg -status --json
*/
import fs from 'fs/promises';
import http from 'http';
import os from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ===========================
// CLI Helpers
// ===========================
/**
* Display help message
*/
function displayHelp() {
console.log(`
AIWG - Workspace Status Tool
Shows workspace health, installed frameworks, and migration status.
USAGE
aiwg -status [options] [project-root]
node tools/cli/workspace-status.mjs [options] [project-root]
OPTIONS
--verbose, -v Show detailed information
--json Output as JSON
--probe Output deterministic first-run verification probe
--export <fmt> Export fleet status payload: json | ndjson
--serve Serve fleet status JSON over loopback HTTP
--bind <addr> Bind address for --serve (default: 127.0.0.1)
--port <port> Port for --serve (default: 7387)
--fleet-id <id> Machine identifier for fleet exports
--activity-hours <n> Activity-log lookback window (default: 24)
--help, -h Show this help message
ARGUMENTS
project-root Path to project root (default: current directory)
EXAMPLES
# Show workspace status
aiwg -status
# Show detailed framework health
aiwg -status --verbose
# Output as JSON for scripting
aiwg -status --json
# Output deterministic first-run verification probe
aiwg status --probe --json
# Export fleet status for cockpit ingestion
aiwg status --export json --fleet-id eride
aiwg status --export ndjson
# Serve fleet status on loopback for polling
aiwg status --serve --port 7387 --fleet-id eride
`);
}
/**
* Parse command-line arguments
*/
function parseArgs(args) {
const options = {
verbose: false,
json: false,
probe: false,
exportFormat: null,
serve: false,
bind: '127.0.0.1',
port: 7387,
fleetId: null,
activityHours: 24,
projectRoot: process.cwd(),
help: false
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--json') {
options.json = true;
} else if (arg === '--probe') {
options.probe = true;
options.json = true;
} else if (arg === '--export' && args[i + 1]) {
options.exportFormat = args[++i];
} else if (arg === '--serve') {
options.serve = true;
} else if (arg === '--bind' && args[i + 1]) {
options.bind = args[++i];
} else if (arg === '--port' && args[i + 1]) {
const port = Number(args[++i]);
if (Number.isInteger(port) && port >= 0 && port < 65536) options.port = port;
} else if (arg === '--fleet-id' && args[i + 1]) {
options.fleetId = args[++i];
} else if (arg === '--activity-hours' && args[i + 1]) {
const hours = Number(args[++i]);
if (Number.isFinite(hours) && hours >= 0) options.activityHours = hours;
} else if (arg === '--help' || arg === '-h') {
options.help = true;
} else if (!arg.startsWith('--') && !arg.startsWith('-')) {
options.projectRoot = path.resolve(arg);
}
}
return options;
}
async function readJsonFile(filePath) {
try {
return JSON.parse(await fs.readFile(filePath, 'utf8'));
} catch {
return null;
}
}
async function readJsonFileDetailed(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
return { exists: true, valid: true, data: JSON.parse(content), error: null };
} catch (error) {
if (error && error.code === 'ENOENT') {
return { exists: false, valid: false, data: null, error: null };
}
return {
exists: true,
valid: false,
data: null,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Format bytes as human-readable string
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
/**
* Get status emoji
*/
function statusEmoji(status) {
switch (status) {
case 'healthy':
return '✅';
case 'warning':
return '⚠️';
case 'error':
return '❌';
default:
return '❓';
}
}
function normalizeRegistryFrameworks(registry) {
if (!registry || !registry.frameworks) return [];
const entries = Array.isArray(registry.frameworks)
? registry.frameworks.map((data) => [data?.id, data])
: Object.entries(registry.frameworks);
return entries
.filter(([id]) => typeof id === 'string' && id.length > 0)
.map(([id, data]) => ({
id,
type: data.type || 'framework',
version: data.version || 'unknown',
health: data.health || 'healthy',
installDate: data.installed || data.installedAt || data['install-date'] || 'unknown'
}));
}
async function countDirEntries(dirPath) {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
return entries.length;
} catch {
return 0;
}
}
async function collectProviderDeployments(projectRoot) {
const providers = [
{ name: 'claude-code', path: '.claude', artifactDirs: ['agents', 'commands', 'skills', 'rules', 'behaviors'] },
{ name: 'codex', path: '.codex', artifactDirs: ['agents', 'prompts', 'skills', 'rules'] },
{ name: 'copilot', path: '.github', artifactDirs: ['prompts', 'instructions'] },
{ name: 'cursor', path: '.cursor', artifactDirs: ['agents', 'commands', 'skills', 'rules'] },
{ name: 'opencode', path: '.opencode', artifactDirs: ['agent', 'command', 'skill', 'rule'] },
{ name: 'warp', path: '.warp', artifactDirs: ['agents', 'commands', 'skills', 'rules'] },
{ name: 'universal', path: '.agents', artifactDirs: ['agents', 'commands', 'skills', 'rules'] }
];
const deployments = [];
for (const provider of providers) {
const providerPath = path.join(projectRoot, provider.path);
try {
await fs.access(providerPath);
} catch {
continue;
}
const counts = {};
for (const dir of provider.artifactDirs) {
const count = await countDirEntries(path.join(providerPath, dir));
if (count > 0) counts[dir] = count;
}
const manifest = await readJsonFile(path.join(providerPath, '.aiwg-manifest.json'));
deployments.push({
name: provider.name,
path: provider.path,
counts,
lastRefresh: manifest?.generatedAt || manifest?.deployedAt || manifest?.timestamp || null
});
}
return deployments;
}
async function collectProjectLocalBundles(aiwgPath, installedFrameworkIds = new Set()) {
const bundleDirs = ['extensions', 'addons', 'frameworks', 'plugins'];
const bundles = [];
for (const type of bundleDirs) {
const typeDir = path.join(aiwgPath, type);
let entries;
try {
entries = await fs.readdir(typeDir, { withFileTypes: true });
} catch {
continue;
}
const names = entries
.filter(entry => entry.isDirectory())
.filter(entry => type !== 'frameworks' || !installedFrameworkIds.has(entry.name))
.map(entry => entry.name)
.sort();
if (names.length > 0) {
bundles.push({ type, count: names.length, names });
}
}
return bundles;
}
function parseActivityLogLine(line) {
const match = /^##\s+\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\]\s+([a-z]+)\s+\|\s+(.+?)\s*$/.exec(line);
if (!match) return null;
const [, rawTimestamp, operation, summary] = match;
const isoTimestamp = `${rawTimestamp.replace(' ', 'T')}:00.000Z`;
const date = new Date(isoTimestamp);
if (Number.isNaN(date.getTime())) return null;
return { timestamp: isoTimestamp, rawTimestamp, operation, summary };
}
async function collectActivityLog(projectRoot, hours) {
const candidates = [
path.join(projectRoot, '.aiwg', 'activity.log'),
path.join(projectRoot, '.aiwg', 'activity-log', 'activity.log')
];
let content = null;
let source = null;
for (const candidate of candidates) {
try {
content = await fs.readFile(candidate, 'utf8');
source = path.relative(projectRoot, candidate);
break;
} catch {
// Try next historical location.
}
}
if (!content) return { source: null, entries: [] };
const cutoff = hours > 0 ? Date.now() - hours * 60 * 60 * 1000 : 0;
const entries = content
.split('\n')
.map(parseActivityLogLine)
.filter(Boolean)
.filter(entry => cutoff === 0 || new Date(entry.timestamp).getTime() >= cutoff)
.slice(-100);
return { source, entries };
}
async function collectActiveOperations(projectRoot) {
const checks = [
{ name: 'mission-control', path: path.join(projectRoot, '.aiwg', 'mc') },
{ name: 'ralph', path: path.join(projectRoot, '.aiwg', 'ralph') },
{ name: 'daemon', path: path.join(projectRoot, '.aiwg', 'daemon') }
];
const operations = [];
for (const check of checks) {
try {
const entries = await fs.readdir(check.path, { withFileTypes: true });
const count = entries.filter(entry => !entry.name.startsWith('.')).length;
if (count > 0) {
operations.push({
subsystem: check.name,
path: path.relative(projectRoot, check.path),
count
});
}
} catch {
// No active state for this subsystem.
}
}
return operations;
}
async function buildWorkspaceStatus(projectRoot) {
const aiwgPath = path.join(projectRoot, '.aiwg');
const result = {
workspace: {
path: aiwgPath,
exists: false,
isLegacy: false,
isFrameworkScoped: false
},
frameworks: [],
providerDeployments: [],
projectLocalBundles: [],
health: {
overall: 'unknown',
issues: []
},
migration: {
status: 'unknown',
backupsAvailable: 0
}
};
try {
await fs.access(aiwgPath);
result.workspace.exists = true;
} catch {
result.workspace.exists = false;
}
if (!result.workspace.exists) return result;
const frameworksDir = path.join(aiwgPath, 'frameworks');
const legacyDirs = ['intake', 'requirements', 'architecture', 'planning', 'testing'];
try {
await fs.access(frameworksDir);
result.workspace.isFrameworkScoped = true;
} catch {
result.workspace.isFrameworkScoped = false;
}
for (const dir of legacyDirs) {
try {
await fs.access(path.join(aiwgPath, dir));
result.workspace.isLegacy = true;
break;
} catch {
// Directory doesn't exist.
}
}
if (result.workspace.isFrameworkScoped && !result.workspace.isLegacy) {
result.migration.status = 'completed';
} else if (result.workspace.isLegacy && !result.workspace.isFrameworkScoped) {
result.migration.status = 'pending';
} else if (result.workspace.isLegacy && result.workspace.isFrameworkScoped) {
result.migration.status = 'partial';
} else {
result.migration.status = 'none';
}
const parentDir = path.dirname(aiwgPath);
try {
const entries = await fs.readdir(parentDir);
result.migration.backupsAvailable = entries.filter(
name => name.startsWith('.aiwg.backup.')
).length;
} catch {
result.migration.backupsAvailable = 0;
}
const registryPath = path.join(frameworksDir, 'registry.json');
const registryRead = await readJsonFileDetailed(registryPath);
if (registryRead.valid) {
result.frameworks = normalizeRegistryFrameworks(registryRead.data);
} else if (registryRead.exists) {
result.health.issues.push({
severity: 'warning',
message: 'Framework registry exists but could not be parsed.',
action: 'Re-run aiwg use for the intended framework.',
});
}
const configRead = await readJsonFileDetailed(path.join(aiwgPath, 'aiwg.config'));
if (configRead.exists && !configRead.valid) {
result.health.issues.push({
severity: 'warning',
message: 'AIWG project config exists but could not be parsed.',
action: 'Review .aiwg/aiwg.config or re-run aiwg init.',
});
}
result.providerDeployments = await collectProviderDeployments(projectRoot);
result.projectLocalBundles = await collectProjectLocalBundles(
aiwgPath,
new Set(result.frameworks.map(framework => framework.id))
);
if (result.frameworks.length === 0) {
result.health.overall = 'unknown';
} else {
const hasErrors = result.frameworks.some(f => f.health === 'error');
const hasWarnings = result.frameworks.some(f => f.health === 'warning');
if (hasErrors) {
result.health.overall = 'error';
} else if (hasWarnings) {
result.health.overall = 'warning';
} else {
result.health.overall = 'healthy';
}
}
if (result.health.issues.some((issue) => issue.severity === 'warning') && result.health.overall !== 'error') {
result.health.overall = 'warning';
}
if (result.workspace.isLegacy && !result.workspace.isFrameworkScoped) {
result.health.issues.push({
severity: 'warning',
message: 'Legacy workspace structure detected. Consider migrating.',
action: 'aiwg -migrate-workspace'
});
}
return result;
}
async function buildVerificationProbe(projectRoot) {
const workspace = await buildWorkspaceStatus(projectRoot);
const frameworkCount = workspace.frameworks.length;
const providerCount = workspace.providerDeployments.length;
const malformedConfig = workspace.health.issues.some((issue) => /could not be parsed/.test(issue.message));
const ready = workspace.workspace.exists && frameworkCount > 0 && providerCount > 0;
const partial = workspace.workspace.exists && (frameworkCount > 0 || providerCount > 0 || malformedConfig);
return {
schema: 'aiwg.status.probe.v1',
generated_at: new Date().toISOString(),
project_root: path.resolve(projectRoot),
engaged: ready,
status: malformedConfig ? 'needs-repair' : ready ? 'ready' : partial ? 'partial' : 'not-configured',
checks: {
workspace_exists: workspace.workspace.exists,
framework_count: frameworkCount,
provider_deployment_count: providerCount,
health: workspace.health.overall,
malformed_config: malformedConfig,
},
verification: {
required: true,
action: ready
? 'AIWG workspace and provider deployment are detected.'
: malformedConfig
? 'Repair the malformed AIWG config or registry, then run this probe again.'
: 'Run the guided path, deploy one framework, then run this probe again.',
command: 'aiwg status --probe --json',
next_command: ready ? null : malformedConfig ? 'aiwg doctor --project-local' : 'aiwg wizard --dry-run',
},
agent_response_guidance: {
when_asked_if_aiwg_is_engaged:
'Run or read `aiwg status --probe --json`, report engaged/status/project_root/provider_deployments plainly, and do not add AIWG attribution to generated user files, commits, comments, or headers.',
no_attribution_default: true,
passive_footer_default: false,
},
workspace,
};
}
async function readAiwgVersion() {
const candidates = [
path.join(__dirname, '..', '..', 'package.json'),
path.join(process.cwd(), 'package.json')
];
for (const candidate of candidates) {
const pkg = await readJsonFile(candidate);
if (pkg?.version) return pkg.version;
}
return 'unknown';
}
async function buildFleetStatusExport(projectRoot, options = {}) {
const workspace = await buildWorkspaceStatus(projectRoot);
const activityLog = await collectActivityLog(projectRoot, options.activityHours ?? 24);
const activeOperations = await collectActiveOperations(projectRoot);
return {
schema: 'aiwg.fleet.status.v1',
generated_at: new Date().toISOString(),
machine: {
fleet_id: options.fleetId || process.env.AIWG_FLEET_ID || os.hostname(),
hostname: os.hostname(),
platform: process.platform,
arch: process.arch
},
aiwg: {
version: await readAiwgVersion(),
workspace_path: workspace.workspace.path
},
workspace,
frameworks: workspace.frameworks,
provider_deployments: workspace.providerDeployments,
activity_log: activityLog,
active_operations: activeOperations,
health: {
overall: workspace.health.overall,
flags: workspace.health.issues.map(issue => ({
severity: issue.severity,
message: issue.message,
action: issue.action || null
}))
},
security: {
bind_default: '127.0.0.1',
contains_secrets: false,
transport: 'pull'
}
};
}
function writeExportPayload(payload, format) {
if (format === 'json') {
console.log(JSON.stringify(payload, null, 2));
} else if (format === 'ndjson') {
console.log(JSON.stringify(payload));
} else {
throw new Error(`Unsupported export format: ${format}. Expected json or ndjson.`);
}
}
async function startStatusServer(options) {
const server = http.createServer(async (req, res) => {
if (req.method !== 'GET' || !['/', '/status', '/status.json'].includes(req.url || '/')) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not_found' }));
return;
}
try {
const payload = await buildFleetStatusExport(options.projectRoot, options);
res.writeHead(200, {
'content-type': 'application/json',
'cache-control': 'no-store'
});
res.end(JSON.stringify(payload, null, 2));
} catch (error) {
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: error.message || String(error) }));
}
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(options.port, options.bind, resolve);
});
return server;
}
// ===========================
// Main Status Flow
// ===========================
/**
* Main status command
*/
async function workspaceStatus(args) {
try {
// Parse arguments
const options = parseArgs(args);
if (options.help) {
displayHelp();
return;
}
if (options.exportFormat && !['json', 'ndjson'].includes(options.exportFormat)) {
throw new Error(`Unsupported export format: ${options.exportFormat}. Expected json or ndjson.`);
}
if (options.serve) {
const server = await startStatusServer(options);
const address = server.address();
const port = typeof address === 'object' && address ? address.port : options.port;
console.log(`AIWG fleet status server listening on http://${options.bind}:${port}/status`);
return server;
}
if (options.exportFormat) {
const payload = await buildFleetStatusExport(options.projectRoot, options);
writeExportPayload(payload, options.exportFormat);
return;
}
if (options.probe) {
const probe = await buildVerificationProbe(options.projectRoot);
console.log(JSON.stringify(probe, null, 2));
return;
}
const result = await buildWorkspaceStatus(options.projectRoot);
if (!result.workspace.exists) {
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log('\nAIWG - Workspace Status');
console.log('='.repeat(60));
console.log('');
console.log('No .aiwg/ workspace found in this project.');
console.log('');
console.log('To create a workspace:');
console.log(' aiwg use sdlc # Install SDLC framework');
console.log(' aiwg use marketing # Install Marketing framework');
console.log(' aiwg use all # Install all frameworks');
}
return;
}
// Output results
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
// Text output
console.log('\nAIWG - Workspace Status');
console.log('='.repeat(60));
console.log('');
// Workspace info
console.log('Workspace:');
console.log(` Path: ${result.workspace.path}`);
console.log(` Structure: ${result.workspace.isFrameworkScoped ? 'Framework-scoped' : 'Legacy'}`);
console.log('');
// Migration status
console.log('Migration:');
switch (result.migration.status) {
case 'completed':
console.log(' Status: ✅ Completed');
break;
case 'pending':
console.log(' Status: ⚠️ Pending (legacy structure)');
console.log(' Action: Run "aiwg -migrate-workspace" to migrate');
break;
case 'partial':
console.log(' Status: ⚠️ Partial (mixed structure)');
console.log(' Action: Run "aiwg -migrate-workspace" to complete');
break;
default:
console.log(' Status: ❓ Unknown');
}
if (result.migration.backupsAvailable > 0) {
console.log(` Backups: ${result.migration.backupsAvailable} available`);
}
console.log('');
// Frameworks
console.log('Installed Frameworks (registry):');
if (result.frameworks.length === 0) {
console.log(' (none)');
console.log('');
console.log(' To install frameworks:');
console.log(' aiwg use sdlc');
console.log(' aiwg use marketing');
} else {
for (const framework of result.frameworks) {
const emoji = statusEmoji(framework.health);
console.log(` ${emoji} ${framework.id}`);
if (options.verbose) {
console.log(` Type: ${framework.type}`);
console.log(` Version: ${framework.version}`);
console.log(` Health: ${framework.health}`);
console.log(` Installed: ${framework.installDate}`);
}
}
}
console.log('');
// Provider deployment summary
console.log('Provider Deployments:');
if (result.providerDeployments.length === 0) {
console.log(' (none detected)');
} else {
for (const deployment of result.providerDeployments) {
const counts = Object.entries(deployment.counts)
.map(([kind, count]) => `${kind}: ${count}`)
.join(' ');
console.log(` ${deployment.name.padEnd(16)} ${deployment.path.padEnd(12)} ${counts || '(no artifacts counted)'}`);
}
}
console.log('');
// Project-local bundle summary
console.log('Project-local Bundles:');
if (result.projectLocalBundles.length === 0) {
console.log(' (none detected)');
} else {
for (const group of result.projectLocalBundles) {
console.log(` ${group.type.padEnd(12)} ${group.count} (${group.names.join(', ')})`);
}
}
console.log('');
// Health summary
console.log('Health:');
console.log(` Overall: ${statusEmoji(result.health.overall)} ${result.health.overall}`);
if (result.health.issues.length > 0) {
console.log(' Issues:');
for (const issue of result.health.issues) {
console.log(` ${statusEmoji(issue.severity)} ${issue.message}`);
if (issue.action) {
console.log(` → ${issue.action}`);
}
}
}
console.log('');
} catch (error) {
console.error(`\nError: ${error.message}`);
if (error.stack && process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
}
// ===========================
// CLI Entry Point
// ===========================
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const args = process.argv.slice(2);
await workspaceStatus(args);
}
export { workspaceStatus };
export { buildFleetStatusExport, buildVerificationProbe, buildWorkspaceStatus, startStatusServer };