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
723 lines • 27.6 kB
JavaScript
/**
* Artifact Index CLI Commands
*
* Provides CLI interface for artifact index operations:
* - build: Build/rebuild the artifact index
* - query: Search artifacts by keyword, type, phase, tags
* - deps: Show artifact dependency graph
* - stats: Show index statistics
*
* Supports multi-graph architecture via --graph flag:
* - framework: AIWG framework source (shared, built during `aiwg use`)
* - project: SDLC artifacts in .aiwg/ (per-project)
* - codebase: Source code, tests, configs (per-project)
*
* @implements #420 #421
* @source @src/cli/handlers/subcommands.ts
* @tests @test/unit/artifacts/cli.test.ts
*/
import { GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { load as loadYaml } from 'js-yaml';
/** Parse --graph flag from args, returns undefined for "all graphs" */
function parseGraphFlag(args) {
const idx = args.indexOf('--graph');
if (idx === -1 || idx + 1 >= args.length)
return undefined;
const val = args[idx + 1];
// Load user-defined graphs so validation is complete
loadUserGraphConfigs(process.cwd());
if (val in GRAPH_CONFIGS)
return val;
const validNames = Object.keys(GRAPH_CONFIGS).join(', ');
console.error(`Error: Invalid graph type '${val}'. Valid: ${validNames}`);
process.exit(1);
}
/**
* Main index command router
*/
export async function main(args) {
const subcommand = args[0];
const subcommandArgs = args.slice(1);
// #1231 — intercept --help/-h before subcommand dispatch. Print the
// same usage block as the no-args case, but framed as help (exit 0)
// rather than as an unknown-subcommand error (exit 1).
if (subcommand === '--help' || subcommand === '-h') {
printIndexUsage();
process.exit(0);
}
switch (subcommand) {
case 'build':
await handleBuild(subcommandArgs);
break;
case 'query':
await handleQuery(subcommandArgs);
break;
case 'discover':
await handleDiscover(subcommandArgs);
break;
case 'show':
await handleShow(subcommandArgs);
break;
case 'deps':
await handleDeps(subcommandArgs);
break;
case 'stats':
await handleStats(subcommandArgs);
break;
case 'neighbors':
await handleNeighbors(subcommandArgs);
break;
case 'set':
await handleSetQuery(subcommandArgs);
break;
case 'watch':
await handleWatch(subcommandArgs);
break;
case 'views': {
const { main: viewsMain } = await import('./views/cli.js');
await viewsMain(subcommandArgs);
break;
}
case 'enrich': {
const { main: enrichMain } = await import('./enrichment/cli.js');
await enrichMain(subcommandArgs);
break;
}
case 'doctor': {
const { main: doctorMain } = await import('./audit/cli.js');
await doctorMain(subcommandArgs);
break;
}
case undefined:
console.error('Error: Index subcommand required');
console.log('');
printIndexUsage();
process.exit(1);
break;
default:
console.error(`Error: Unknown index subcommand '${subcommand}'`);
console.log('Available: build, query, discover, deps, stats, neighbors, set, watch');
process.exit(1);
}
}
function printIndexUsage() {
console.log('Usage: aiwg index <subcommand> [options]');
console.log('');
console.log('Available subcommands:');
console.log(' build Build/rebuild the artifact index');
console.log(' query Search artifacts by keyword, type, phase, tags');
console.log(' discover Capability search across skills/agents/commands/rules (#1214)');
console.log(' show Print the full text of a specific skill/agent/command/rule');
console.log(' deps Show artifact dependency graph');
console.log(' stats Show index statistics');
console.log(' neighbors Get neighbors of a node in a graph');
console.log(' set Set operations (intersection, union, difference) on neighbor sets');
console.log(' watch Start a filesystem watcher for automatic incremental index updates');
console.log('');
console.log('Options:');
console.log(' --graph <name> Target a specific graph (framework, project, codebase, or user-defined)');
console.log(' --all Build all known graphs (including user-defined)');
console.log('');
console.log('Examples:');
console.log(' aiwg index build');
console.log(' aiwg index build --all');
console.log(' aiwg index build --graph codebase --force');
console.log(' aiwg index discover "create intake"');
console.log(' aiwg index discover "deploy production" --limit 5 --json');
console.log(' aiwg index discover "audit security" --type skill');
console.log(' aiwg index show skill intake-wizard');
console.log(' aiwg index show skill flow-deploy-to-production --json');
console.log(' aiwg index show agent aiwg-steward');
console.log(' aiwg index query "authentication" --type use-case');
console.log(' aiwg index query "security rules" --graph framework --json');
console.log(' aiwg index deps .aiwg/requirements/UC-001.md');
console.log(' aiwg index stats --json');
console.log(' aiwg index stats --graph project');
console.log(' aiwg index neighbors --graph citation-network --node REF-008 --direction in --edge-type cites');
console.log(' aiwg index set --graph citation-network --op intersection --node-a REF-008 --node-b REF-016 --direction in');
}
/**
* Handle 'index watch' command — filesystem watcher daemon for auto-index updates.
*
* Modes:
* aiwg index watch — start watcher (foreground)
* aiwg index watch --stop — stop a running watcher
* aiwg index watch --status — check watcher status
*
* @implements #795
*/
async function handleWatch(args) {
if (args.includes('--help') || args.includes('-h')) {
console.log('Usage: aiwg index watch [options]');
console.log('');
console.log('Start a filesystem watcher that triggers incremental index rebuilds');
console.log('when .aiwg/ files change. Uses the checksum manifest (#794) for fast');
console.log('change detection.');
console.log('');
console.log('Options:');
console.log(' --stop Stop a running watcher for this project');
console.log(' --status Show whether a watcher is running');
console.log(' --debounce <ms> Debounce window for batched updates (default: 500)');
console.log(' --graph <name> Graph to rebuild (default: project)');
console.log(' --verbose Log every detected change');
console.log('');
console.log('Examples:');
console.log(' aiwg index watch');
console.log(' aiwg index watch --verbose --debounce 1000');
console.log(' aiwg index watch --stop');
console.log(' aiwg index watch --status');
return;
}
const cwd = process.cwd();
const { startWatcher, stopWatcher, getRunningPid } = await import('./watcher.js');
// --status: check if a watcher is running
if (args.includes('--status')) {
const pid = getRunningPid(cwd);
if (pid) {
console.log(`Watcher running: PID ${pid}`);
}
else {
console.log('No watcher running for this project');
}
return;
}
// --stop: terminate a running watcher
if (args.includes('--stop')) {
const stopped = stopWatcher(cwd);
if (stopped) {
console.log('Watcher stopped');
}
else {
console.log('No watcher running for this project');
}
return;
}
// --debounce <ms>
let debounceMs = 500;
const debounceIdx = args.indexOf('--debounce');
if (debounceIdx !== -1 && debounceIdx + 1 < args.length) {
const parsed = parseInt(args[debounceIdx + 1], 10);
if (Number.isFinite(parsed) && parsed > 0) {
debounceMs = parsed;
}
}
// --graph <name>
const graph = parseGraphFlag(args);
const verbose = args.includes('--verbose');
try {
startWatcher({
cwd,
debounceMs,
verbose,
graph,
});
// startWatcher registers SIGINT/SIGTERM handlers; block the main thread
// until one of them fires. setInterval keeps Node alive indefinitely.
setInterval(() => { }, 1 << 30);
}
catch (err) {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}
/**
* Handle 'index build' command
*/
async function handleBuild(args) {
if (args.includes('--help') || args.includes('-h')) {
console.log('Usage: aiwg index build [options]');
console.log('');
console.log('Options:');
console.log(' --force Full rebuild (ignore checksums, re-index everything)');
console.log(' --verbose Show detailed progress during indexing');
console.log(' --all Build all known graphs (including user-defined)');
console.log(' --scope <dir> Limit scan to a specific subdirectory');
console.log(' --graph <name> Build a specific graph only (built-in or user-defined)');
console.log('');
console.log('Built-in graph names: project, codebase, framework');
console.log('User-defined graphs: configure under index.graphs in .aiwg/config.yaml');
console.log('');
console.log('Default behavior (no --graph): builds all graphs with defaultBuild: true');
console.log(' Built-in defaults: project (always), codebase (skipped if src/test/tools absent)');
console.log('');
console.log('Examples:');
console.log(' aiwg index build');
console.log(' aiwg index build --force');
console.log(' aiwg index build --graph codebase --force');
console.log(' aiwg index build --graph references # user-defined graph');
console.log(' aiwg index build --scope documentation/references');
console.log(' aiwg index build --all');
return;
}
// Dynamic import to keep the CLI router lightweight
const { buildIndex } = await import('./index-builder.js');
const cwd = process.cwd();
const force = args.includes('--force');
const verbose = args.includes('--verbose');
const all = args.includes('--all');
const graph = parseGraphFlag(args);
let scope;
const scopeIdx = args.indexOf('--scope');
if (scopeIdx !== -1 && scopeIdx + 1 < args.length) {
scope = args[scopeIdx + 1];
}
// Load user-defined graphs
loadUserGraphConfigs(cwd);
if (graph) {
// Build a specific graph — explicitly requested via --graph
await buildIndex(cwd, { force, verbose, scope, graph, explicit: true });
}
else if (all) {
// Build all known graphs — user asked for everything, but don't hard-error on missing dirs
for (const name of Object.keys(GRAPH_CONFIGS)) {
await buildIndex(cwd, { force, verbose, graph: name, explicit: false });
}
}
else {
// Default: build graphs with defaultBuild=true; skip gracefully if their dirs don't exist
for (const [name, config] of Object.entries(GRAPH_CONFIGS)) {
if (config.defaultBuild) {
await buildIndex(cwd, { force, verbose, scope: name === Object.keys(GRAPH_CONFIGS)[0] ? scope : undefined, graph: name, explicit: false });
}
}
}
maybePrintMarkdownIndicesHint(cwd);
}
function hasMarkdownIndicesManifest(cwd) {
const configPath = join(cwd, '.aiwg', 'config.yaml');
if (!existsSync(configPath))
return false;
try {
const config = loadYaml(readFileSync(configPath, 'utf8'));
const index = config?.index;
const graphs = index?.graphs;
const indices = graphs?.indices;
return Array.isArray(indices?.manifest) && indices.manifest.length > 0;
}
catch {
return false;
}
}
function maybePrintMarkdownIndicesHint(cwd) {
if (!hasMarkdownIndicesManifest(cwd))
return;
console.log('');
console.log('Note: markdown indices declared in index.graphs.indices.manifest are not rendered by `aiwg index build`.');
console.log(' To render them, use the corpus-index-build skill:');
console.log(' aiwg discover "build research indices"');
console.log(' aiwg show skill corpus-index-build');
}
/**
* Handle 'index query' command
*
* Stub — full implementation in #416
*/
async function handleQuery(args) {
const { queryIndex } = await import('./query-engine.js');
const cwd = process.cwd();
// Parse query text (positional args before any -- flags)
const textParts = [];
const flags = [];
let inFlags = false;
for (const arg of args) {
if (arg.startsWith('--')) {
inFlags = true;
}
if (inFlags) {
flags.push(arg);
}
else {
textParts.push(arg);
}
}
const text = textParts.join(' ') || undefined;
const json = flags.includes('--json');
// Parse filter flags
let type;
let phase;
let tags;
let updatedAfter;
let limit;
let pathPattern;
for (let i = 0; i < flags.length; i++) {
if (flags[i] === '--type' && i + 1 < flags.length) {
type = flags[++i];
}
else if (flags[i] === '--phase' && i + 1 < flags.length) {
phase = flags[++i];
}
else if (flags[i] === '--tags' && i + 1 < flags.length) {
tags = flags[++i];
}
else if (flags[i] === '--updated-after' && i + 1 < flags.length) {
updatedAfter = flags[++i];
}
else if (flags[i] === '--limit' && i + 1 < flags.length) {
limit = parseInt(flags[++i], 10);
}
else if (flags[i] === '--path' && i + 1 < flags.length) {
pathPattern = flags[++i];
}
}
const graph = parseGraphFlag(flags);
await queryIndex(cwd, {
text,
type,
phase,
tags: tags?.split(','),
updatedAfter,
limit,
path: pathPattern,
}, { json, graph });
}
/**
* Handle 'index deps' command
*
* Stub — full implementation in #417
*/
async function handleDeps(args) {
const { showDeps } = await import('./dep-graph.js');
const cwd = process.cwd();
// First non-flag arg is the artifact path
const artifactPath = args.find(a => !a.startsWith('--'));
if (!artifactPath) {
console.error('Error: Artifact path required');
console.log('Usage: aiwg index deps <path> [--direction upstream|downstream|both] [--depth N] [--json]');
process.exit(1);
}
const json = args.includes('--json');
let direction = 'both';
const dirIdx = args.indexOf('--direction');
if (dirIdx !== -1 && dirIdx + 1 < args.length) {
const val = args[dirIdx + 1];
if (val === 'upstream' || val === 'downstream' || val === 'both') {
direction = val;
}
}
let depth = 3;
const depthIdx = args.indexOf('--depth');
if (depthIdx !== -1 && depthIdx + 1 < args.length) {
depth = parseInt(args[depthIdx + 1], 10);
}
let edgeType;
const etIdx = args.indexOf('--edge-type');
if (etIdx !== -1 && etIdx + 1 < args.length) {
edgeType = args[etIdx + 1];
}
const graph = parseGraphFlag(args);
await showDeps(cwd, artifactPath, { direction, depth, json, graph, edgeType });
}
/**
* Handle 'index stats' command
*
* Stub — full implementation in #418
*/
async function handleStats(args) {
const { showStats } = await import('./stats.js');
const cwd = process.cwd();
const json = args.includes('--json');
const graph = parseGraphFlag(args);
await showStats(cwd, { json, graph });
}
/**
* Handle 'index neighbors' command
*
* @implements #725
*/
async function handleNeighbors(args) {
if (args.includes('--help') || args.includes('-h')) {
console.log('Usage: aiwg index neighbors --graph <name> --node <id> [options]');
console.log('');
console.log('Options:');
console.log(' --graph <name> Graph to query (required)');
console.log(' --node <id> Node path or REF-XXX identifier (required)');
console.log(' --direction <dir> in (upstream), out (downstream), or both (default: both)');
console.log(' --edge-type <type> Filter by edge type (e.g., "cites", "depends-on")');
console.log(' --json Output as JSON');
console.log('');
console.log('Examples:');
console.log(' aiwg index neighbors --graph citation-network --node REF-008 --direction in --edge-type cites');
console.log(' aiwg index neighbors --graph project --node .aiwg/requirements/UC-001.md --json');
return;
}
const { showNeighbors } = await import('./graph-query.js');
const cwd = process.cwd();
const graph = parseGraphFlag(args);
if (!graph) {
console.error('Error: --graph is required for neighbors command');
process.exit(1);
}
let node;
const nodeIdx = args.indexOf('--node');
if (nodeIdx !== -1 && nodeIdx + 1 < args.length) {
node = args[nodeIdx + 1];
}
if (!node) {
// Try first positional arg
node = args.find(a => !a.startsWith('--') && args.indexOf(a) !== args.indexOf('--graph') + 1);
}
if (!node) {
console.error('Error: --node is required for neighbors command');
process.exit(1);
}
let direction = 'both';
const dirIdx = args.indexOf('--direction');
if (dirIdx !== -1 && dirIdx + 1 < args.length) {
const val = args[dirIdx + 1];
if (val === 'in' || val === 'out' || val === 'both') {
direction = val;
}
}
let edgeType;
const etIdx = args.indexOf('--edge-type');
if (etIdx !== -1 && etIdx + 1 < args.length) {
edgeType = args[etIdx + 1];
}
const json = args.includes('--json');
await showNeighbors(cwd, { graph, node, direction, edgeType, json });
}
/**
* Handle 'index set' command
*
* @implements #725
*/
async function handleSetQuery(args) {
if (args.includes('--help') || args.includes('-h')) {
console.log('Usage: aiwg index set --graph <name> --op <operation> --node-a <id> --node-b <id> [options]');
console.log('');
console.log('Operations:');
console.log(' intersection Nodes in both neighbor sets');
console.log(' union Nodes in either neighbor set');
console.log(' difference Nodes in A but not in B');
console.log('');
console.log('Options:');
console.log(' --graph <name> Graph to query (required)');
console.log(' --op <operation> Set operation (required)');
console.log(' --node-a <id> First node (required)');
console.log(' --node-b <id> Second node (required)');
console.log(' --direction <dir> in (upstream) or out (downstream) (default: in)');
console.log(' --edge-type <type> Filter by edge type');
console.log(' --json Output as JSON');
console.log('');
console.log('Examples:');
console.log(' # Papers that cited both REF-008 and REF-016');
console.log(' aiwg index set --graph citation-network --op intersection --node-a REF-008 --node-b REF-016 --direction in');
console.log('');
console.log(' # Papers cited by REF-004 but not cited by REF-001');
console.log(' aiwg index set --graph citation-network --op difference --node-a REF-004 --node-b REF-001 --direction out');
return;
}
const { executeSetQuery } = await import('./graph-query.js');
const cwd = process.cwd();
const graph = parseGraphFlag(args);
if (!graph) {
console.error('Error: --graph is required for set command');
process.exit(1);
}
let op;
const opIdx = args.indexOf('--op');
if (opIdx !== -1 && opIdx + 1 < args.length) {
op = args[opIdx + 1];
}
if (!op || !['intersection', 'union', 'difference'].includes(op)) {
console.error('Error: --op is required (intersection, union, difference)');
process.exit(1);
}
let nodeA;
const naIdx = args.indexOf('--node-a');
if (naIdx !== -1 && naIdx + 1 < args.length)
nodeA = args[naIdx + 1];
let nodeB;
const nbIdx = args.indexOf('--node-b');
if (nbIdx !== -1 && nbIdx + 1 < args.length)
nodeB = args[nbIdx + 1];
if (!nodeA || !nodeB) {
console.error('Error: --node-a and --node-b are required');
process.exit(1);
}
let direction = 'in';
const dirIdx = args.indexOf('--direction');
if (dirIdx !== -1 && dirIdx + 1 < args.length) {
const val = args[dirIdx + 1];
if (val === 'in' || val === 'out')
direction = val;
}
let edgeType;
const etIdx = args.indexOf('--edge-type');
if (etIdx !== -1 && etIdx + 1 < args.length) {
edgeType = args[etIdx + 1];
}
const json = args.includes('--json');
await executeSetQuery(cwd, {
graph,
op: op,
nodeA,
nodeB,
direction,
edgeType,
json,
});
}
/**
* Handle 'index discover' command — capability-search for AIWG skills,
* agents, commands, and rules.
*
* Like `query` but tuned for capability lookups: ranks by trigger
* phrase + capability description first, falls back to title/tag/path
* matches. Defaults to AIWG artifact kinds (skill/agent/command/rule),
* narrowable via `--type`.
*
* Returns a token-tight format intended for in-context agent
* consumption — path, score, type, top trigger, capability snippet.
*
* @implements #1214
*/
async function handleDiscover(args) {
const { discoverCapability } = await import('./query-engine.js');
const cwd = process.cwd();
// Parse positional phrase (everything before flags)
const textParts = [];
const flags = [];
let inFlags = false;
for (const arg of args) {
if (arg.startsWith('--'))
inFlags = true;
if (inFlags)
flags.push(arg);
else
textParts.push(arg);
}
const phrase = textParts.join(' ').trim();
if (!phrase) {
console.error('Error: aiwg index discover requires a search phrase');
console.log('');
console.log('Usage: aiwg index discover "<phrase>" [--type <kinds>] [--limit N] [--json] [--graph <name>]');
console.log('');
console.log('Examples:');
console.log(' aiwg index discover "create intake"');
console.log(' aiwg index discover "deploy production" --limit 5');
console.log(' aiwg index discover "audit security" --type skill,agent');
console.log(' aiwg index discover "intake" --json');
process.exit(1);
}
// Parse flags
let typeFilter;
// K=5 default — see query-engine.ts comment (#1218 Wave A).
let limit = 5;
let json = false;
for (let i = 0; i < flags.length; i++) {
if (flags[i] === '--type' && i + 1 < flags.length) {
typeFilter = flags[++i].split(',').map(s => s.trim()).filter(Boolean);
}
else if (flags[i] === '--limit' && i + 1 < flags.length) {
const n = parseInt(flags[++i], 10);
if (!Number.isNaN(n) && n > 0)
limit = n;
}
else if (flags[i] === '--json') {
json = true;
}
}
const graph = parseGraphFlag(flags);
await discoverCapability(cwd, {
phrase,
typeFilter,
limit,
json,
graph,
});
}
/**
* Handle 'index show' command — print the full text of a specific
* artifact by type and name.
*
* Shape (#1218):
* aiwg show <type> <name> [--json] [--first] [--graph <name>]
*
* Type is positional (not a flag) so the verb reads as
* "show <kind> <name>". `<type>` is one of: skill, agent, command, rule.
*
* Companion to `discover`: where discover ranks candidates, show fetches
* the artifact body so consumers don't need to navigate the filesystem.
*/
async function handleShow(args) {
const { showArtifact } = await import('./query-engine.js');
const cwd = process.cwd();
const positional = [];
const flags = [];
let inFlags = false;
for (const arg of args) {
if (arg.startsWith('--'))
inFlags = true;
if (inFlags)
flags.push(arg);
else
positional.push(arg);
}
const ALLOWED_TYPES = ['skill', 'agent', 'command', 'rule'];
const HELP_TEXT = [
'',
'Usage: aiwg show <type> <name> [--json] [--first] [--graph <name>]',
' aiwg index show <type> <name> ...',
'',
'Types: skill | agent | command | rule',
'',
'Examples:',
' aiwg show skill intake-wizard',
' aiwg show skill flow-deploy-to-production --json',
' aiwg show agent aiwg-steward',
' aiwg show command discover',
'',
'Tip: use `aiwg discover "<phrase>"` first to find the right name.',
].join('\n');
if (positional.length === 0) {
console.error('Error: aiwg show requires a type and name');
console.error(HELP_TEXT);
process.exit(1);
}
// Wave A (#1218): if the first positional is a known type, treat it
// as the type. If it's NOT a known type, fall through to single-name
// mode — `aiwg show intake-wizard` works as long as the name is
// unambiguous across artifact types. Multi-type matches still error
// with the disambiguation list (existing behavior in showArtifact).
let type = null;
let name;
const firstLower = positional[0].toLowerCase();
if (ALLOWED_TYPES.includes(firstLower)) {
type = firstLower;
name = positional.slice(1).join(' ').trim();
if (!name) {
console.error(`Error: aiwg show ${type} requires a name`);
console.error(HELP_TEXT);
process.exit(1);
}
}
else {
// Single-name fallback. Pass to showArtifact without a type filter
// so its existing ambiguity logic kicks in: unique match → succeed,
// multiple matches → list candidates and exit 2 (or pick first
// with `--first`).
name = positional.join(' ').trim();
}
let json = false;
let first = false;
for (let i = 0; i < flags.length; i++) {
if (flags[i] === '--json') {
json = true;
}
else if (flags[i] === '--first') {
first = true;
}
}
const graph = parseGraphFlag(flags);
await showArtifact(cwd, {
name,
typeFilter: type ? [type] : undefined,
json,
first,
graph,
});
}
//# sourceMappingURL=cli.js.map