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
149 lines • 4.82 kB
JavaScript
/**
* View definition + result filesystem store.
*
* Layout:
* .aiwg/index/views/
* ├── <name>.yaml # definition
* └── results/
* ├── <name>.json # last computed result
* └── <name>.meta.json # freshness metadata
*
* @implements #1207
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
import { join } from 'node:path';
import { dumpViewYaml, parseViewYaml } from './definition.js';
const VIEWS_ROOT_DEFAULT = '.aiwg/index/views';
const NAME_RE = /^[a-z0-9][a-z0-9-_]*$/;
export function resolveViewsRoot(cwd = process.cwd()) {
return join(cwd, VIEWS_ROOT_DEFAULT);
}
function defPath(root, name) {
if (!NAME_RE.test(name))
throw new Error(`Invalid view name: ${name}`);
return join(root, `${name}.yaml`);
}
function resultPath(root, name) {
return join(root, 'results', `${name}.json`);
}
function metaPath(root, name) {
return join(root, 'results', `${name}.meta.json`);
}
/** Persist a definition; creates the views directory if needed. */
export function putDefinition(root, def) {
mkdirSync(root, { recursive: true });
const file = defPath(root, def.name);
writeFileSync(file, dumpViewYaml(def), 'utf-8');
return file;
}
/** Read a definition by name. Throws on missing or invalid. */
export function getDefinition(root, name) {
const file = defPath(root, name);
if (!existsSync(file))
throw new Error(`No view: ${name}`);
return parseViewYaml(readFileSync(file, 'utf-8'));
}
/** Remove a definition and its results. */
export function removeView(root, name) {
let defRemoved = false;
let resultRemoved = false;
const dp = defPath(root, name);
if (existsSync(dp)) {
rmSync(dp);
defRemoved = true;
}
const rp = resultPath(root, name);
const mp = metaPath(root, name);
if (existsSync(rp)) {
rmSync(rp);
resultRemoved = true;
}
if (existsSync(mp))
rmSync(mp);
return { defRemoved, resultRemoved };
}
/** List all views with freshness summary. */
export function listViews(root, now = new Date()) {
if (!existsSync(root))
return [];
const out = [];
for (const file of readdirSync(root)) {
if (!file.endsWith('.yaml'))
continue;
const name = file.replace(/\.yaml$/, '');
if (!NAME_RE.test(name))
continue;
let def;
try {
def = parseViewYaml(readFileSync(join(root, file), 'utf-8'));
}
catch {
continue;
}
const meta = readMeta(root, name);
const summary = {
name: def.name,
producer: def.producer,
aggregate: def.aggregate,
hasResults: meta !== null,
builtAt: meta?.builtAt ?? null,
ageDays: meta ? ageDays(meta.builtAt, now) : null,
staleReason: classifyStaleness(def, meta, now),
};
out.push(summary);
}
return out.sort((a, b) => a.name.localeCompare(b.name));
}
/** Read a view's stored result. Throws on missing. */
export function getResult(root, name) {
const rp = resultPath(root, name);
if (!existsSync(rp))
throw new Error(`No results for view: ${name}`);
const meta = readMeta(root, name);
if (!meta)
throw new Error(`No metadata for view: ${name}`);
const result = JSON.parse(readFileSync(rp, 'utf-8'));
return { name, result, meta };
}
/** Persist a view result + metadata atomically. */
export function putResult(root, name, result, meta) {
mkdirSync(join(root, 'results'), { recursive: true });
writeFileSync(resultPath(root, name), JSON.stringify(result, null, 2), 'utf-8');
writeFileSync(metaPath(root, name), JSON.stringify(meta, null, 2), 'utf-8');
}
function readMeta(root, name) {
const mp = metaPath(root, name);
if (!existsSync(mp))
return null;
try {
return JSON.parse(readFileSync(mp, 'utf-8'));
}
catch {
return null;
}
}
function ageDays(iso, now) {
return Math.max(0, Math.floor((now.getTime() - new Date(iso).getTime()) / 86_400_000));
}
function classifyStaleness(def, meta, now) {
if (!meta)
return 'never-built';
const days = ageDays(meta.builtAt, now);
switch (def.refresh.schedule) {
case 'daily':
if (days >= 1)
return 'stale-schedule';
break;
case 'weekly':
if (days >= 7)
return 'stale-schedule';
break;
case 'monthly':
if (days >= 30)
return 'stale-schedule';
break;
case 'never': break;
}
return 'fresh';
}
//# sourceMappingURL=store.js.map