import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { writeZipFromDirectory } from '../../_shared/lib/zip'; import config from '../project.config'; const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const rootDir = path.resolve(projectDir, '../..'); const templatePath = path.join(projectDir, 'skill', 'slm-design.skill.md'); const outputDir = path.join(rootDir, 'public', config.slug, 'skill'); const skillZipPath = path.join(outputDir, 'slm-design.skill.zip'); const description = 'Use this skill ONLY when working in a project that uses SLM Design (Scoped Layered Module Design) — a frontend architecture with layers app, layouts, screens, widgets, business, infra, ui, shared; modules with public API via index.ts; business-domain factories; and segments like ui/, parts/, hooks/, services/, mappers/. Apply when designing new modules, deciding where to place code, reviewing imports and dependency direction, refactoring existing SLM code, or planning monorepo placement (apps/, packages/ui, packages/infra, packages/shared). Project signals that SLM is active (strongest first): src/screens/ + src/widgets/ combination (highly specific to SLM — FSD uses pages/, Atomic and Clean don\'t have this layer); *.factory.ts files in business modules with index.ts exporting only factory and type-only exports; src/screens/ alongside src/ui/ and src/shared/; in monorepo, the same structure inside apps/{app}/src/. Note: src/business/ is a strong confirmation when present, but small or early-stage SLM projects may not have it yet — absence of business/ does NOT rule out SLM. Conflicting signals indicating other architectures: src/features/ or src/entities/ or src/pages/ (FSD); src/atoms/ or src/molecules/ (Atomic); src/domain/ or src/useCases/ (Clean). User signals (Russian or English): "SLM", "SLM Design", "Scoped Layered Module Design", "куда положить", "where to place", "какой слой", "which layer", "это модуль или компонент", "module or component", "можно ли так импортировать", "can I import", "deep import", "фабрика", "factory", "публичный API", "public API", "parts/", "business-домен", "business domain", "композиция фабрик", "factory composition". Do NOT use this skill for: projects on Feature-Sliced Design (FSD), Atomic Design, Clean Architecture, or any other frontend architecture — SLM has its own rules and is NOT a synonym for these; legacy codebases without explicit SLM structure (do not propose migration unless the user explicitly asks); small isolated tasks like styling, single-file bug fixes, CSS, or build tooling where architectural placement is not the question; backend architecture. When project architecture is ambiguous, ask the user before applying SLM rules.'; const canonPages = [ { anchor: 'canon-architecture-index', source: 'canons/architecture/index.md', }, { anchor: 'canon-architecture-layers', source: 'canons/architecture/layers.md', }, { anchor: 'canon-architecture-modules', source: 'canons/architecture/modules.md', }, { anchor: 'canon-architecture-segments', source: 'canons/architecture/segments.md', }, { anchor: 'canon-architecture-monorepo', source: 'canons/architecture/monorepo.md', }, { anchor: 'canon-examples-react-factory', source: 'canons/examples/react/factory.md', }, { anchor: 'canon-examples-react-factory-composition', source: 'canons/examples/react/factory-composition.md', }, { anchor: 'canon-examples-react-composition-provider', source: 'canons/examples/react/composition-provider.md', }, ] as const; const routeAnchors = new Map([ ['/architecture', 'canon-architecture-index'], ['/architecture/', 'canon-architecture-index'], ['/architecture/layers', 'canon-architecture-layers'], ['/architecture/modules', 'canon-architecture-modules'], ['/architecture/segments', 'canon-architecture-segments'], ['/architecture/monorepo', 'canon-architecture-monorepo'], ['/examples/react/factory', 'canon-examples-react-factory'], ['/examples/react/factory-composition', 'canon-examples-react-factory-composition'], ['/examples/react/composition-provider', 'canon-examples-react-composition-provider'], ]); function stripFrontmatter(content: string) { return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); } function transformOutsideCode(content: string, transformLine: (line: string) => string) { let inFence = false; return content .split('\n') .map((line) => { if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; return line; } if (inFence) return line; return transformLine(line); }) .join('\n'); } function shiftHeadings(content: string) { return transformOutsideCode(content, (line) => line.replace(/^(#{1,6})\s/, '##$1 ')); } function rewriteMarkdownLinks(content: string) { return transformOutsideCode(content, (line) => line.replace(/\]\((\/[^)\s#]+\/?)(#[^)\s]+)?\)/g, (match, route: string, hash = '') => { const normalizedRoute = route.length > 1 && route.endsWith('/') ? route.slice(0, -1) : route; const anchor = routeAnchors.get(route) ?? routeAnchors.get(normalizedRoute); if (!anchor) return match; return `](${hash || `#${anchor}`})`; })); } function prepareCanon(source: string, anchor: string) { const sourcePath = path.join(projectDir, source); const content = fs.readFileSync(sourcePath, 'utf8'); const prepared = rewriteMarkdownLinks(shiftHeadings(stripFrontmatter(content).trim())); return `\n\n${prepared}`; } function createSkillBody() { const template = fs.readFileSync(templatePath, 'utf8').trim(); const canonContent = canonPages.map((page) => prepareCanon(page.source, page.anchor)).join('\n\n---\n\n'); return template.replace('', canonContent); } function createSkill(frontmatter: string) { return `${frontmatter.trim()}\n\n${createSkillBody()}\n`; } function writeFile(filePath: string, content: string) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, 'utf8'); } function buildSkill() { const frontmatter = `---\nname: slm-design\ndescription: ${description}\n---`; const skillContent = createSkill(frontmatter); fs.rmSync(outputDir, { recursive: true, force: true }); writeFile(path.join(outputDir, '.claude', 'skills', 'slm-design', 'SKILL.md'), skillContent); writeFile(path.join(outputDir, '.opencode', 'skills', 'slm-design', 'SKILL.md'), skillContent); fs.rmSync(skillZipPath, { force: true }); writeZipFromDirectory(outputDir, skillZipPath, ''); console.log(`Собран ${path.relative(rootDir, outputDir)}`); console.log(`Собран ${path.relative(rootDir, skillZipPath)}`); } buildSkill();