- Move src/ → plugin/ with subdirectories: - plugin/core/ (business logic, models, store, permissions, utils, memory) - plugin/tools/ (query, resources) - plugin/commands/ (placeholder for slash commands) - plugin/hooks/ (placeholder for lifecycle hooks) - plugin/index.ts (wiring layer only, no business logic) - Add install.mjs with --install, --uninstall, --openclaw-profile-path - Add skills/ and docs/ root directories - Move planning docs (PLAN.md, FEAT.md, AGENT_TASKS.md) to docs/ - Remove old scripts/install.sh - Update tsconfig rootDir: src → plugin - Update README.md and README.zh.md with new layout - Bump version to 0.2.0 - All tests pass
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { YonexusError } from '../core/models/errors';
|
|
import type { DocsScope, DocsTopic } from '../core/models/types';
|
|
import { slug } from '../core/utils/id';
|
|
|
|
const TOPICS: DocsTopic[] = ['docs', 'notes', 'knowledge', 'rules', 'lessons', 'workflows'];
|
|
|
|
function ensureDirs(base: string, dirs: string[]): void {
|
|
for (const d of dirs) fs.mkdirSync(path.join(base, d), { recursive: true });
|
|
}
|
|
|
|
export class ResourceLayout {
|
|
constructor(private readonly rootDir: string) {}
|
|
|
|
get organizationsRoot(): string {
|
|
return path.join(this.rootDir, 'organizations');
|
|
}
|
|
|
|
orgPath(orgName: string): string {
|
|
return path.join(this.organizationsRoot, slug(orgName));
|
|
}
|
|
|
|
teamPath(orgName: string, teamName: string): string {
|
|
return path.join(this.orgPath(orgName), 'teams', slug(teamName));
|
|
}
|
|
|
|
agentPath(orgName: string, teamName: string, agentId: string): string {
|
|
return path.join(this.teamPath(orgName, teamName), 'agents', slug(agentId));
|
|
}
|
|
|
|
ensureOrganization(orgName: string): void {
|
|
const root = this.orgPath(orgName);
|
|
ensureDirs(root, ['teams', ...TOPICS]);
|
|
}
|
|
|
|
ensureTeam(orgName: string, teamName: string): void {
|
|
const root = this.teamPath(orgName, teamName);
|
|
ensureDirs(root, ['agents', ...TOPICS]);
|
|
}
|
|
|
|
ensureAgent(orgName: string, teamName: string, agentId: string): void {
|
|
const root = this.agentPath(orgName, teamName, agentId);
|
|
ensureDirs(root, TOPICS);
|
|
}
|
|
|
|
private readTopicFiles(topicRoot: string, keyword: string): string[] {
|
|
if (!fs.existsSync(topicRoot)) return [];
|
|
let re: RegExp;
|
|
try {
|
|
re = new RegExp(keyword);
|
|
} catch {
|
|
throw new YonexusError('VALIDATION_ERROR', 'invalid_regex', { keyword });
|
|
}
|
|
|
|
const entries = fs.readdirSync(topicRoot, { withFileTypes: true });
|
|
return entries
|
|
.filter((e) => e.isFile() && re.test(e.name))
|
|
.map((e) => path.join(topicRoot, e.name));
|
|
}
|
|
|
|
getDocs(scope: DocsScope, topic: DocsTopic, keyword: string): string {
|
|
const groups: Record<'ORG' | 'DEPT' | 'TEAM' | 'AGENT', string[]> = {
|
|
ORG: [],
|
|
DEPT: [],
|
|
TEAM: [],
|
|
AGENT: []
|
|
};
|
|
|
|
const orgsRoot = this.organizationsRoot;
|
|
if (!fs.existsSync(orgsRoot)) {
|
|
return '----ORG\n----DEPT\n----TEAM\n----AGENT';
|
|
}
|
|
|
|
const orgs = fs.readdirSync(orgsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
|
|
for (const orgName of orgs) {
|
|
const orgPath = path.join(orgsRoot, orgName);
|
|
if (scope === 'organization') {
|
|
groups.ORG.push(...this.readTopicFiles(path.join(orgPath, topic), keyword));
|
|
}
|
|
|
|
const teamsRoot = path.join(orgPath, 'teams');
|
|
if (!fs.existsSync(teamsRoot)) continue;
|
|
|
|
const teams = fs.readdirSync(teamsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
for (const teamName of teams) {
|
|
const teamPath = path.join(teamsRoot, teamName);
|
|
if (scope === 'team') {
|
|
groups.TEAM.push(...this.readTopicFiles(path.join(teamPath, topic), keyword));
|
|
}
|
|
|
|
const agentsRoot = path.join(teamPath, 'agents');
|
|
if (!fs.existsSync(agentsRoot)) continue;
|
|
|
|
const agents = fs.readdirSync(agentsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
for (const agentId of agents) {
|
|
if (scope === 'agent') {
|
|
groups.AGENT.push(...this.readTopicFiles(path.join(agentsRoot, agentId, topic), keyword));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// department folders are not defined in this layout; reserved empty group for compatible output.
|
|
const printGroup = (k: 'ORG' | 'DEPT' | 'TEAM' | 'AGENT'): string =>
|
|
[`----${k}`, ...groups[k]].join('\n');
|
|
|
|
return [printGroup('ORG'), printGroup('DEPT'), printGroup('TEAM'), printGroup('AGENT')].join('\n');
|
|
}
|
|
}
|