Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/v1-components/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Commonly used exports from `remote` include:

- Do not import from `@retailcrm/embed-ui-v1-components/host` in normal extension code.
- Do not rely on internal file paths from this repository.
- Do not treat Storybook examples or internal source layout as stable runtime API.
- Do not treat examples or internal source layout as stable runtime API.
- Do not assume every host-side component is available to extension authors.

## Example
Expand Down Expand Up @@ -123,12 +123,12 @@ const save = () => {}
- Machine-oriented package summary:
[`./docs/AI.md`](./docs/AI.md)
- Page composition guidelines:
[`./docs/AGENT-DESIGN-GUIDELINES.md`](./docs/AGENT-DESIGN-GUIDELINES.md)
[`./docs/profiles/pages`](./docs/profiles/pages)
- Component profiles:
[`./docs/PROFILES.md`](./docs/PROFILES.md)

For table, catalog, registry, journal, or search-result screens, read [`./docs/AI.md`](./docs/AI.md)
and [`./docs/AGENT-DESIGN-GUIDELINES.md`](./docs/AGENT-DESIGN-GUIDELINES.md), then check the
and [`./docs/profiles/pages/EntityListPage.yml`](./docs/profiles/pages/EntityListPage.yml), then check the
`UiTable` and `UiLink` profiles before generating code. Put filters above the table, persist
filters and pagination in GET query parameters when routing exists, and set `size="small"` on
`UiLink` inside table cells by default.
7 changes: 3 additions & 4 deletions packages/v1-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,14 @@ import { UiButton } from '@retailcrm/embed-ui-v1-components/remote'
- [`docs/README.md`](./docs/README.md) — обзор пакета и правил использования.
- [`docs/COMPONENTS.md`](./docs/COMPONENTS.md) — карта публичных компонентов.
- [`docs/AI.md`](./docs/AI.md) — контекст для ИИ и автоматизаций.
- [`docs/PROFILES.md`](./docs/PROFILES.md) — AI-friendly YAML-профили компонентов.
- [`docs/PROFILES.md`](./docs/PROFILES.md) — AI-friendly YAML-профили компонентов и страниц.
- [`docs/FORMAT.md`](./docs/FORMAT.md) — формат описания компонента для AI-агентов.
- [`docs/AGENT-DESIGN-GUIDELINES.md`](./docs/AGENT-DESIGN-GUIDELINES.md) — правила построения страниц,
модалок, шторок, фильтров и таблиц.
- [`docs/profiles/pages`](./docs/profiles/pages) — YAML-профили страниц, модалок, шторок, фильтров и таблиц.

## AI и инициализация `AGENTS.md`

После установки пакет показывает подсказку, что внутри есть `README.md`, `AGENTS.md`,
AI-заметки и YAML-профили компонентов.
AI-заметки и YAML-профили компонентов и страниц.

Если в целевом проекте еще нет `AGENTS.md`, можно сгенерировать стартовый файл командой:

Expand Down
277 changes: 227 additions & 50 deletions packages/v1-components/bin/embed-ui-v1-components.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#!/usr/bin/env node

import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'

const PACKAGE_NAME = '@retailcrm/embed-ui-v1-components'
const DEFAULT_NEWLINE = '\n'
const AGENTS_SECTION_HEADER = '## @retailcrm/embed-ui-v1-components'
const AGENTS_SECTION_START = '<!-- embed-ui-agents:start -->'
const AGENTS_SECTION_END = '<!-- embed-ui-agents:end -->'

const HELP_TEXT = `Usage:
npx ${PACKAGE_NAME} init-agents [target] [options]
Expand All @@ -21,6 +24,136 @@ Examples:
npx ${PACKAGE_NAME} init-agents --force
`

const toPosixPath = (value) => value.split(path.sep).join('/')

const withDotPrefix = (value) => {
if (!value || value === '.') {
return '.'
}

return value.startsWith('.') ? value : `./${value}`
}

const isPackageRoot = (directory) => {
const packageJsonPath = path.join(directory, 'package.json')

if (!fs.existsSync(packageJsonPath)) {
return false
}

try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))

return packageJson.name === PACKAGE_NAME
} catch {
return false
}
}

const isInsideDirectory = (parent, child) => {
const relativePath = path.relative(parent, child)

return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))
}

const addNestedPackageCandidates = (start, addCandidate) => {
const packagePath = path.join('node_modules', ...PACKAGE_NAME.split('/'))
const ignoredDirectories = new Set(['.git', '.yarn', 'dist', 'node_modules'])
const queue = [path.resolve(start)]
const seen = new Set(queue)

for (let index = 0; index < queue.length; index++) {
const current = queue[index]

addCandidate(path.join(current, packagePath))

let entries = []

try {
entries = fs.readdirSync(current, { withFileTypes: true })
} catch {
continue
}

for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || ignoredDirectories.has(entry.name)) {
continue
}

const next = path.join(current, entry.name)

if (!seen.has(next)) {
seen.add(next)
queue.push(next)
}
}
}
}

const getCurrentPackageRoot = () => {
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')

return isPackageRoot(packageRoot) ? packageRoot : null
}

const findPackageRoot = (target) => {
const candidates = []
const seen = new Set()

const addCandidate = (candidate) => {
const resolved = path.resolve(candidate)

if (!seen.has(resolved)) {
seen.add(resolved)
candidates.push(resolved)
}
}

const addNodeModulesCandidates = (start) => {
const packagePath = path.join('node_modules', ...PACKAGE_NAME.split('/'))
let current = path.resolve(start)

while (true) {
addCandidate(path.join(current, packagePath))

const parent = path.dirname(current)

if (parent === current) {
break
}

current = parent
}
}

addCandidate(target)
addNodeModulesCandidates(target)
addNestedPackageCandidates(target, addCandidate)

const currentPackageRoot = getCurrentPackageRoot()

if (currentPackageRoot && isInsideDirectory(target, currentPackageRoot)) {
addCandidate(currentPackageRoot)
}

const packageRoot = candidates.find(isPackageRoot)

if (!packageRoot) {
throw new Error(
`Cannot find local ${PACKAGE_NAME}. Install it in this project or run init-agents from the package workspace.`
)
}

return packageRoot
}

const createPackageDocsPath = (target) => {
const packageRoot = findPackageRoot(target)
const relativePath = path.relative(target, packageRoot)

return withDotPrefix(toPosixPath(relativePath))
}

const parseArgs = (argv) => {
const options = {
command: null,
Expand Down Expand Up @@ -67,91 +200,134 @@ const parseArgs = (argv) => {
return options
}

const createAgentsTemplate = () => {
const createAgentsTemplate = (packageDocsPath) => {
return `# AGENTS.md

${createAgentsSection(packageDocsPath)}`
}

const createAgentsSection = (packageDocsPath) => {
return `${AGENTS_SECTION_START}
${AGENTS_SECTION_HEADER}

When working with \`${PACKAGE_NAME}\` in this project:

1. Read \`./node_modules/${PACKAGE_NAME}/README.md\`.
2. Then read \`./node_modules/${PACKAGE_NAME}/AGENTS.md\`.
3. Then read \`./node_modules/${PACKAGE_NAME}/docs/AI.md\`.
4. Then read \`./node_modules/${PACKAGE_NAME}/docs/COMPONENTS.md\`.
5. Then open the relevant profile from \`./node_modules/${PACKAGE_NAME}/docs/profiles/<Component>.yml\`.
6. Prefer those docs and profiles over guessing from internal implementation files.
7. Import only from documented public entrypoints:
1. Read \`${packageDocsPath}/README.md\`.
2. Then read \`${packageDocsPath}/AGENTS.md\`.
3. Then read \`${packageDocsPath}/docs/AI.md\`.
4. Then read \`${packageDocsPath}/docs/COMPONENTS.md\`.
5. Then read \`${packageDocsPath}/docs/PROFILES.md\`.
6. Then open relevant component profiles from \`${packageDocsPath}/docs/profiles/components/*.yml\`.
7. For complete pages, modals, sidebars, filters, tables, or settings layouts, open the relevant
page profile from \`${packageDocsPath}/docs/profiles/pages/*.yml\`.
8. Prefer those docs and profiles over guessing from internal implementation files.
9. Import only from documented public entrypoints:
- \`${PACKAGE_NAME}/remote\`
- \`${PACKAGE_NAME}/host\`
- \`${PACKAGE_NAME}/assets/...\`
8. Prefer \`${PACKAGE_NAME}/remote\` for extension UI code.
9. Do not import from package-internal files such as \`dist/*\`, repository-only paths, or source internals.
10. Prefer \`${PACKAGE_NAME}/remote\` for extension UI code.
11. Do not import from package-internal files such as \`dist/*\`, repository-only paths, or source internals.

## Suggested Reading Order

1. \`README.md\`
2. \`AGENTS.md\`
3. \`docs/AI.md\`
4. \`docs/COMPONENTS.md\`
5. The relevant profile from \`docs/profiles/*.yml\`
6. \`docs/FORMAT.md\` if you need to understand profile structure
7. Storybook and public types only when no profile exists yet
` + DEFAULT_NEWLINE
5. \`docs/PROFILES.md\`
6. The relevant component profile from \`docs/profiles/components/*.yml\`
7. The relevant page profile from \`docs/profiles/pages/*.yml\` for full-screen or overlay composition
8. \`docs/FORMAT.md\` if you need to understand profile structure
9. Public type declarations only when no profile exists yet
${AGENTS_SECTION_END}
`
}

const createAgentsSection = () => {
return `${AGENTS_SECTION_HEADER}
const findMarkedSectionRange = (content) => {
const start = content.indexOf(AGENTS_SECTION_START)
const end = content.indexOf(AGENTS_SECTION_END, start + AGENTS_SECTION_START.length)

When working with \`${PACKAGE_NAME}\` in this project:
if (start === -1 && end === -1) {
return null
}

1. Read \`./node_modules/${PACKAGE_NAME}/README.md\`.
2. Then read \`./node_modules/${PACKAGE_NAME}/AGENTS.md\`.
3. Then read \`./node_modules/${PACKAGE_NAME}/docs/AI.md\`.
4. Then read \`./node_modules/${PACKAGE_NAME}/docs/COMPONENTS.md\`.
5. Then open the relevant profile from \`./node_modules/${PACKAGE_NAME}/docs/profiles/<Component>.yml\`.
6. Prefer those docs and profiles over guessing from internal implementation files.
7. Import only from documented public entrypoints:
- \`${PACKAGE_NAME}/remote\`
- \`${PACKAGE_NAME}/host\`
- \`${PACKAGE_NAME}/assets/...\`
8. Prefer \`${PACKAGE_NAME}/remote\` for extension UI code.
9. Do not import from package-internal files such as \`dist/*\`, repository-only paths, or source internals.
if (start === -1 || end === -1 || end < start) {
throw new Error(`AGENTS.md contains incomplete ${PACKAGE_NAME} section markers`)
}

## Suggested Reading Order
return {
start,
end: end + AGENTS_SECTION_END.length,
}
}

1. \`README.md\`
2. \`AGENTS.md\`
3. \`docs/AI.md\`
4. \`docs/COMPONENTS.md\`
5. The relevant profile from \`docs/profiles/*.yml\`
6. \`docs/FORMAT.md\` if you need to understand profile structure
7. Storybook and public types only when no profile exists yet
`
const findLegacySectionRange = (content) => {
const escapedHeader = AGENTS_SECTION_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const headerPattern = new RegExp(`(^|\\n)${escapedHeader}(?=\\n|$)`, 'u')
const match = headerPattern.exec(content)

if (!match) {
return null
}

const start = match.index + match[1].length
const afterHeader = content.slice(start + AGENTS_SECTION_HEADER.length)
const nextExternalHeading = /\n## (?!Suggested Reading Order\b)[^\n]*/u.exec(afterHeader)

return {
start,
end: nextExternalHeading
? start + AGENTS_SECTION_HEADER.length + nextExternalHeading.index
: content.length,
}
}

const hasPackageSection = (content) => content.includes(AGENTS_SECTION_HEADER)
const hasPackageSection = (content) => {
return Boolean(findMarkedSectionRange(content) || findLegacySectionRange(content))
}

const appendSection = (content, section) => {
const trimmed = content.replace(/\s+$/u, '')

if (!trimmed.length) {
return `${section}${DEFAULT_NEWLINE}`
return `${section.trimEnd()}${DEFAULT_NEWLINE}`
}

return `${trimmed}${DEFAULT_NEWLINE}${DEFAULT_NEWLINE}${section}${DEFAULT_NEWLINE}`
return `${trimmed}${DEFAULT_NEWLINE}${DEFAULT_NEWLINE}${section.trimEnd()}${DEFAULT_NEWLINE}`
}

const replaceRange = (content, range, section) => {
const before = content.slice(0, range.start).replace(/\s+$/u, '')
const after = content.slice(range.end).replace(/^\s+/u, '')
const parts = []

if (before) {
parts.push(before)
}

parts.push(section.trimEnd())

if (after) {
parts.push(after)
}

return `${parts.join(`${DEFAULT_NEWLINE}${DEFAULT_NEWLINE}`)}${DEFAULT_NEWLINE}`
}

const replaceSection = (content, section) => {
const escapedHeader = AGENTS_SECTION_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const sectionPattern = new RegExp(`${escapedHeader}[\\s\\S]*?(?=\\n##\\s|$)`, 'u')
const markedRange = findMarkedSectionRange(content)

if (markedRange) {
return replaceRange(content, markedRange, section)
}

const legacyRange = findLegacySectionRange(content)

if (!sectionPattern.test(content)) {
return appendSection(content, section)
if (legacyRange) {
return replaceRange(content, legacyRange, section)
}

return content
.replace(sectionPattern, section.trimEnd())
.replace(/\s+$/u, '') + DEFAULT_NEWLINE
return appendSection(content, section)
}

const initAgents = (target, force) => {
Expand All @@ -166,10 +342,11 @@ const initAgents = (target, force) => {
}

const agentsPath = path.join(target, 'AGENTS.md')
const section = createAgentsSection()
const packageDocsPath = createPackageDocsPath(target)
const section = createAgentsSection(packageDocsPath)

if (!fs.existsSync(agentsPath)) {
fs.writeFileSync(agentsPath, createAgentsTemplate(), 'utf8')
fs.writeFileSync(agentsPath, createAgentsTemplate(packageDocsPath), 'utf8')

console.log(`AGENTS.md was created at ${agentsPath}`)
console.log('Next step: review it and adjust project-specific rules if needed.')
Expand Down
Loading
Loading