refactor: restructure project layout and add install.mjs
- 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
This commit is contained in:
111
plugin/tools/resources.ts
Normal file
111
plugin/tools/resources.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user