diff --git a/skills/commit-workflow/SKILL.md b/.agents/skills/commit-workflow/SKILL.md similarity index 100% rename from skills/commit-workflow/SKILL.md rename to .agents/skills/commit-workflow/SKILL.md diff --git a/skills/local-ci-simulation/SKILL.md b/.agents/skills/local-ci-simulation/SKILL.md similarity index 100% rename from skills/local-ci-simulation/SKILL.md rename to .agents/skills/local-ci-simulation/SKILL.md diff --git a/skills/local-ci-simulation/agents/openai.yaml b/.agents/skills/local-ci-simulation/agents/openai.yaml similarity index 100% rename from skills/local-ci-simulation/agents/openai.yaml rename to .agents/skills/local-ci-simulation/agents/openai.yaml diff --git a/skills/sync-remote-host-registry/SKILL.md b/.agents/skills/sync-remote-host-registry/SKILL.md similarity index 100% rename from skills/sync-remote-host-registry/SKILL.md rename to .agents/skills/sync-remote-host-registry/SKILL.md diff --git a/skills/yarn-lock-conflict-resolution/SKILL.md b/.agents/skills/yarn-lock-conflict-resolution/SKILL.md similarity index 100% rename from skills/yarn-lock-conflict-resolution/SKILL.md rename to .agents/skills/yarn-lock-conflict-resolution/SKILL.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63bd5037..c2201962 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,9 @@ jobs: - name: Build worktree run: yarn workspaces foreach -A --topological-dev run build + - name: Ensure root bin is generated + run: test -s bin/embed-ui.mjs + - name: Run eslint run: yarn eslint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 360c88c1..9f0380c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,9 @@ jobs: - name: Build worktree run: yarn workspaces foreach -A --topological-dev run build + - name: Ensure root bin is generated + run: test -s bin/embed-ui.mjs + - name: Run eslint run: yarn eslint @@ -67,6 +70,9 @@ jobs: - name: Build worktree run: yarn workspaces foreach -A --topological-dev run build + - name: Ensure root bin is generated + run: test -s bin/embed-ui.mjs + - name: Run tests run: yarn test diff --git a/.gitignore b/.gitignore index 355e15f6..ee13811c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ generated node_modules coverage artifacts/ +/bin/ +packages/v1-endpoint/docs/targets/ .codex .npmrc diff --git a/.npmignore b/.npmignore index 5db7c059..b22d3580 100644 --- a/.npmignore +++ b/.npmignore @@ -23,6 +23,7 @@ eslint.config.js tsconfig.json tsconfig.test.json vite.config.ts +vite.config.bin.ts vitest.config.ts yarn.lock diff --git a/AGENTS.md b/AGENTS.md index 1180a22f..27cf448d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,15 +93,16 @@ yarn workspace @retailcrm/embed-ui-v1-components run storybook:build ``` ## Commit Workflow -- Before creating commits, you must read `skills/commit-workflow/SKILL.md` and follow its rules. +- Before creating commits, you must read `.agents/skills/commit-workflow/SKILL.md` and follow its rules. ## Skills -- Repository-local skills are available under `skills/`. +- Repository-local skills are available under `.agents/skills/`. - If a skill conflicts with this file, follow `AGENTS.md`. - Current local skills: - - `skills/commit-workflow/SKILL.md` - - `skills/sync-remote-host-registry/SKILL.md` - - `skills/yarn-lock-conflict-resolution/SKILL.md` + - `.agents/skills/commit-workflow/SKILL.md` + - `.agents/skills/local-ci-simulation/SKILL.md` + - `.agents/skills/sync-remote-host-registry/SKILL.md` + - `.agents/skills/yarn-lock-conflict-resolution/SKILL.md` ## Notes - Do not assume legacy rules from other repositories (especially `omnica`) apply here. diff --git a/README.md b/README.md index 59a2441f..c241a3a7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ API и компоненты для создания расширений инт - `v1-components` — UI-компоненты и сопутствующая документация, [README.md пакета](./packages/v1-components/README.md). - `v1-contexts` — реактивные контексты и доступ к данным RetailCRM JS API, [README.md пакета](./packages/v1-contexts/README.md). -- `v1-endpoint` — endpoint API для remote-страниц и виджетов, [README.md пакета](./packages/v1-endpoint/README.md). +- `v1-endpoint` — endpoint API для встраиваемых страниц и виджетов, [README.md пакета](./packages/v1-endpoint/README.md). - `v1-testing` — тестовые утилиты и вспомогательные типы для интеграций, [README.md пакета](./packages/v1-testing/README.md). - `v1-types` — базовые type declarations для публичного API, [README.md пакета](./packages/v1-types/README.md). @@ -21,7 +21,100 @@ API и компоненты для создания расширений инт npx @retailcrm/embed-ui-v1-components init-agents ``` -## Обновление версий в целевом проекте +## CLI `@retailcrm/embed-ui` + +Пакет поставляет бинарник `embed-ui`, который можно запускать через `npx`. +CLI умеет обновлять версии пакетов, добавлять пакеты в существующий проект и выполнять начальную инициализацию +фронтенда расширения. + +```bash +npx @retailcrm/embed-ui --help +``` + +### Начальная Инициализация + +Команда `init` создает или дополняет проект фронтенда расширения: + +- добавляет `package.json`, если его еще нет; +- добавляет зависимости `@retailcrm/embed-ui*`, `vue`, `pinia`, `vue-i18n` и dev-зависимости для Vite, TypeScript и ESLint; +- создает `tsconfig.json`, `vite.config.ts`, `eslint.config.js`, `env.d.ts`; +- создает стартовые файлы во frontend-каталоге: endpoint worker, страницу настроек, виджет заказа, i18n JSON-сообщения; +- добавляет `extensionrc.json` и `scripts/publish-extension.mjs`; +- добавляет `AGENTS.md` и MCP-настройки пакетов, если они не отключены флагами. + +```bash +npx @retailcrm/embed-ui init ./web --package-manager yarn +``` + +Если не указывать дополнительные флаги, `init` работает так: + +- корень проекта — текущий рабочий каталог; +- frontend-каталог — позиционный аргумент команды, а если его нет, то `./src` для нового проекта или `./web`, если `./src` уже существует; +- версия пакетов — последняя опубликованная версия из npm; +- package manager — определяется по единственному lock-файлу в корне проекта, в интерактивном терминале запрашивается у пользователя, в неинтерактивном режиме используется `npm`; +- пакеты — `@retailcrm/embed-ui`, `v1-components`, `v1-contexts`, `v1-types` и `v1-endpoint`; +- установка зависимостей — запускается автоматически после изменения `package.json`; +- шаблон — создается стартовая конфигурация для страницы настроек и виджета `order/card:common.after`; +- каталоги — создаются `endpoint`, `pages`, `widgets`, `shared`, `i18n` и `i18n/locales` внутри frontend-каталога; +- `AGENTS.md` — создается или дополняется инструкциями корневого пакета и выбранных пакетов; +- MCP — добавляется `.mcp.json` для `v1-endpoint`; +- client configs для Cursor, Junie и VS Code — не создаются без `--mcp-client-configs`; +- существующие файлы не перезаписываются, а зависимости с потенциальным конфликтом не заменяются без явных force-флагов. + +По умолчанию команда использует текущий рабочий каталог как корень проекта. Его можно задать явно: + +```bash +npx @retailcrm/embed-ui init ./web --cwd ./my-project --package-manager npm +``` + +Если в проекте уже есть `src`, для нового фронтенда расширения удобнее использовать отдельный каталог, например +`./web`. CLI не перезаписывает существующие файлы без явного разрешения и выводит предупреждения о пропущенных шагах. + +Полезные флаги: + +- `--no-install` — не запускать установку зависимостей после изменения `package.json`. +- `--version 0.9.21` — использовать указанную версию пакетов вместо последней версии из npm. +- `--exact` — записывать точные версии вместо диапазонов. +- `--packages embed-ui,components,contexts,types,endpoint` — явно выбрать пакеты для установки и настройки. +- `--force-deps` — заменить несовместимые версии зависимостей. +- `--fix-sections` — перенести зависимости в ожидаемые секции `dependencies`/`devDependencies`. +- `--force-files` — перезаписать генерируемые стартовые файлы. +- `--no-template` — не создавать стартовые Vue-файлы и `extensionrc.json`. +- `--no-agents` — не создавать и не дополнять `AGENTS.md`. +- `--no-mcp` — не добавлять MCP-настройки пакетов. +- `--mcp-client-configs cursor,junie,vscode` — дополнительно создать project-level конфиги поддерживаемых AI-клиентов. + +`--force` включает силовой режим для управляемых частей, но учитывает флаги `--no-*`. Например, можно обновить только +агентские инструкции и MCP-настройки без перегенерации стартовых файлов: + +```bash +npx @retailcrm/embed-ui init ./web --force --no-install --no-template +``` + +### AGENTS.md И MCP + +При `init` CLI добавляет общий раздел в `AGENTS.md`, а пакеты могут добавить свои инструкции. Сейчас это используют: + +- `@retailcrm/embed-ui-v1-components` — добавляет порядок чтения README, AI-документации и YAML-профилей компонентов. +- `@retailcrm/embed-ui-v1-endpoint` — добавляет инструкции по целям виджетов и MCP-ресурсам. + +Для `v1-endpoint` также создается `.mcp.json` с сервером `retailcrm-embed-ui-v1-endpoint`. Он отдает AI-friendly +описания целей виджетов через ресурсы: + +- `embed-ui-v1-endpoint://targets`; +- `embed-ui-v1-endpoint://targets/`. + +Конфиги конкретных клиентов не создаются по умолчанию, потому что в них чаще бывают пользовательские правки. +Чтобы добавить только поддерживаемые project-level конфиги, укажите: + +```bash +npx @retailcrm/embed-ui init ./web --no-install --mcp-client-configs cursor,junie,vscode +``` + +Повторный запуск без `--force` не дублирует управляемые секции и записи MCP. При `--force` обновляются только записи +этого пакета, ручные серверы и соседние разделы не удаляются. + +### Обновление Версий Можно запустить бинарник через `npx`, чтобы обновить пакеты `@retailcrm/embed-ui*` во всех `package.json` текущего рабочего дерева или выбранного поддерева @@ -37,6 +130,8 @@ npx @retailcrm/embed-ui --target ./my-project --dry-run По умолчанию используется последняя версия из npm. Флаг `--exact` переключает формат обновления на точную версию. CLI сохраняет исходные отступы и переводы строк в каждом изменяемом `package.json`. +### Добавление Пакетов + Для точечного добавления пакетов только в один `package.json` есть флаг `--add`. Если не передать `--packages`, CLI откроет интерактивный режим с выбором пакетов и кратким описанием. @@ -46,6 +141,9 @@ npx @retailcrm/embed-ui --add --packages components,contexts npx @retailcrm/embed-ui --add --target ./my-project --version 0.9.11 ``` +Доступные идентификаторы пакетов: `embed-ui`, `components`, `contexts`, `types`, `endpoint`. +Пакет `testing` пока не добавляется через публичную инициализацию. + ## Цели встраивания Цели встраивания определяют места, где будет размещена разметка виджета, генерируемая расширением. diff --git a/bin/embed-ui-update.mjs b/bin/embed-ui-update.mjs deleted file mode 100755 index 20469ed9..00000000 --- a/bin/embed-ui-update.mjs +++ /dev/null @@ -1,617 +0,0 @@ -#!/usr/bin/env node - -import { createInterface } from 'node:readline/promises' -import { execFileSync } from 'node:child_process' -import fs from 'node:fs' -import path from 'node:path' -import { pathToFileURL } from 'node:url' -import process from 'node:process' - -export const ROOT_PACKAGE = '@retailcrm/embed-ui' -export const TARGET_SECTIONS = [ - 'dependencies', - 'devDependencies', - 'peerDependencies', - 'optionalDependencies', -] - -export const INSTALLABLE_PACKAGES = [ - { - id: 'embed-ui', - name: ROOT_PACKAGE, - section: 'dependencies', - description: 'Базовый пакет с общим API и согласованными v1-зависимостями.', - }, - { - id: 'components', - name: '@retailcrm/embed-ui-v1-components', - section: 'dependencies', - description: 'UI-компоненты для host/remote приложений.', - }, - { - id: 'contexts', - name: '@retailcrm/embed-ui-v1-contexts', - section: 'dependencies', - description: 'Реактивные контексты RetailCRM JS API.', - }, - { - id: 'types', - name: '@retailcrm/embed-ui-v1-types', - section: 'dependencies', - description: 'Базовые type declarations для RetailCRM JS API.', - }, - { - id: 'testing', - name: '@retailcrm/embed-ui-v1-testing', - section: 'devDependencies', - description: 'Вспомогательные утилиты и типы для тестов интеграций.', - }, - { - id: 'endpoint', - name: '@retailcrm/embed-ui-v1-endpoint', - section: 'dependencies', - description: 'Endpoint API для интеграций в RetailCRM.', - }, -] - -const DEFAULT_INDENT = ' ' -const DEFAULT_NEWLINE = '\n' -const SKIP_DIRECTORIES = new Set([ - '.git', - '.hg', - '.svn', - '.yarn', - 'node_modules', - 'dist', - 'build', - 'coverage', -]) - -const HELP_TEXT = `Usage: - npx @retailcrm/embed-ui [target] [version] [options] - -Options: - -t, --target Target path (default: current directory) - -v, --version Target version. If omitted, latest npm version is used - --exact Use exact version instead of range - --dry-run Show changes without writing package.json - --add Add selected embed-ui packages into one package.json - --packages Comma-separated package ids or names for --add - -h, --help Show this help - -Examples: - npx @retailcrm/embed-ui - npx @retailcrm/embed-ui --version 0.9.11 - npx @retailcrm/embed-ui ./my-project 0.9.11 - npx @retailcrm/embed-ui --target ./my-project --dry-run - npx @retailcrm/embed-ui --add - npx @retailcrm/embed-ui --add --packages components,contexts -` - -const isSemverLike = (value) => /^v?\d+\.\d+\.\d+/.test(value) -const stripLeadingV = (value) => value.replace(/^v/, '') - -const parsePackageList = (value) => - value - .split(',') - .map((entry) => entry.trim()) - .filter(Boolean) - -export const parseArgs = (argv) => { - const options = { - target: process.cwd(), - version: null, - dryRun: false, - exact: false, - add: false, - packages: null, - } - - const positionals = [] - - for (let index = 0; index < argv.length; index++) { - const argument = argv[index] - - if (argument === '-h' || argument === '--help') { - console.log(HELP_TEXT) - process.exit(0) - } - - if (argument === '-t' || argument === '--target') { - const value = argv[index + 1] - if (!value) { - throw new Error('Option --target requires a value') - } - - options.target = path.resolve(process.cwd(), value) - index++ - continue - } - - if (argument === '-v' || argument === '--version') { - const value = argv[index + 1] - if (!value) { - throw new Error('Option --version requires a value') - } - - options.version = stripLeadingV(value) - index++ - continue - } - - if (argument === '--packages') { - const value = argv[index + 1] - if (!value) { - throw new Error('Option --packages requires a value') - } - - options.packages = parsePackageList(value) - index++ - continue - } - - if (argument === '--dry-run') { - options.dryRun = true - continue - } - - if (argument === '--exact') { - options.exact = true - continue - } - - if (argument === '--add') { - options.add = true - continue - } - - if (argument.startsWith('-')) { - throw new Error(`Unknown option: ${argument}`) - } - - positionals.push(argument) - } - - if (positionals.length > 2) { - throw new Error('Too many positional arguments') - } - - if (positionals.length >= 1) { - const first = positionals[0] - if (!options.version && isSemverLike(first)) { - options.version = stripLeadingV(first) - } else { - options.target = path.resolve(process.cwd(), first) - } - } - - if (positionals.length === 2) { - if (options.version) { - throw new Error('Version is already specified') - } - - options.version = stripLeadingV(positionals[1]) - } - - if (options.packages && !options.add) { - throw new Error('Option --packages can only be used together with --add') - } - - return options -} - -export const resolveLatestVersion = () => { - const output = execFileSync( - 'npm', - ['view', ROOT_PACKAGE, 'version'], - { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - } - ).trim() - - if (!output) { - throw new Error(`Cannot resolve latest version for ${ROOT_PACKAGE}`) - } - - return output -} - -export const isTargetPackage = (name) => - name === ROOT_PACKAGE || name.startsWith(`${ROOT_PACKAGE}-`) - -const createRange = (version, exact) => exact ? version : `^${version}` - -export const formatRange = (currentRange, nextVersion, exact) => { - if (exact) { - return nextVersion - } - - if (currentRange.startsWith('workspace:')) { - return currentRange - } - - if (currentRange.startsWith('~')) { - return `~${nextVersion}` - } - - if (currentRange.startsWith('^')) { - return `^${nextVersion}` - } - - return `^${nextVersion}` -} - -export const detectFormatting = (source) => { - const newline = source.includes('\r\n') ? '\r\n' : DEFAULT_NEWLINE - const indentMatch = source.match(/\n([ \t]+)"/) - - return { - indent: indentMatch?.[1] ?? DEFAULT_INDENT, - newline, - trailingNewline: source.endsWith('\n') || source.endsWith('\r\n'), - } -} - -export const serializePackageJson = (packageJson, formatting) => { - const serialized = JSON.stringify(packageJson, null, formatting.indent) - .replace(/\n/g, formatting.newline) - - return formatting.trailingNewline - ? `${serialized}${formatting.newline}` - : serialized -} - -const ensureDirectoryExists = (targetPath) => { - if (!fs.existsSync(targetPath)) { - throw new Error(`Path not found: ${targetPath}`) - } - - const stat = fs.statSync(targetPath) - if (!stat.isDirectory()) { - throw new Error(`Target is not a directory: ${targetPath}`) - } -} - -export const resolvePackageJsonPath = (targetPath) => { - if (path.basename(targetPath) === 'package.json') { - if (!fs.existsSync(targetPath)) { - throw new Error(`package.json not found: ${targetPath}`) - } - - return targetPath - } - - const packageJsonPath = path.resolve(targetPath, 'package.json') - - if (!fs.existsSync(packageJsonPath)) { - throw new Error(`package.json not found: ${packageJsonPath}`) - } - - return packageJsonPath -} - -export const collectPackageJsonPaths = (targetPath) => { - const resolvedTarget = path.resolve(targetPath) - - if (!fs.existsSync(resolvedTarget)) { - throw new Error(`Path not found: ${resolvedTarget}`) - } - - if (path.basename(resolvedTarget) === 'package.json') { - return [resolvedTarget] - } - - ensureDirectoryExists(resolvedTarget) - - const packageJsonPaths = [] - - const visit = (directoryPath) => { - const packageJsonPath = path.join(directoryPath, 'package.json') - if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).isFile()) { - packageJsonPaths.push(packageJsonPath) - } - - for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) { - if (!entry.isDirectory() || entry.isSymbolicLink()) { - continue - } - - if (SKIP_DIRECTORIES.has(entry.name)) { - continue - } - - visit(path.join(directoryPath, entry.name)) - } - } - - visit(resolvedTarget) - - return packageJsonPaths.sort() -} - -const findDependencySection = (packageJson, packageName) => { - for (const section of TARGET_SECTIONS) { - const dependencyMap = packageJson[section] - - if (dependencyMap && typeof dependencyMap === 'object' && packageName in dependencyMap) { - return section - } - } - - return null -} - -export const updatePackageJson = (packageJson, version, exact) => { - const updates = [] - - for (const section of TARGET_SECTIONS) { - const dependencyMap = packageJson[section] - - if (!dependencyMap || typeof dependencyMap !== 'object') { - continue - } - - for (const [name, currentRange] of Object.entries(dependencyMap)) { - if (!isTargetPackage(name) || typeof currentRange !== 'string') { - continue - } - - const nextRange = formatRange(currentRange, version, exact) - if (nextRange === currentRange) { - continue - } - - dependencyMap[name] = nextRange - updates.push({ - type: 'update', - section, - name, - currentRange, - nextRange, - }) - } - } - - return updates -} - -export const resolveInstallPackages = (tokens) => { - const selectedPackages = [] - const seen = new Set() - - for (const token of tokens) { - const normalized = token.trim() - if (!normalized) { - continue - } - - const numericIndex = Number(normalized) - const selectedPackage = - Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= INSTALLABLE_PACKAGES.length - ? INSTALLABLE_PACKAGES[numericIndex - 1] - : INSTALLABLE_PACKAGES.find((entry) => entry.id === normalized || entry.name === normalized) - - if (!selectedPackage) { - const supported = INSTALLABLE_PACKAGES - .map((entry, index) => `${index + 1}/${entry.id}/${entry.name}`) - .join(', ') - - throw new Error(`Unknown add target "${normalized}". Supported values: ${supported}`) - } - - if (seen.has(selectedPackage.name)) { - continue - } - - seen.add(selectedPackage.name) - selectedPackages.push(selectedPackage) - } - - return selectedPackages -} - -export const installPackages = (packageJson, packages, version, exact) => { - const updates = [] - - for (const selectedPackage of packages) { - const section = findDependencySection(packageJson, selectedPackage.name) ?? selectedPackage.section - const dependencyMap = packageJson[section] ?? {} - - if (!(section in packageJson)) { - packageJson[section] = dependencyMap - } - - const currentRange = dependencyMap[selectedPackage.name] - const nextRange = typeof currentRange === 'string' - ? formatRange(currentRange, version, exact) - : createRange(version, exact) - - if (currentRange === nextRange) { - continue - } - - dependencyMap[selectedPackage.name] = nextRange - updates.push({ - type: typeof currentRange === 'string' ? 'update' : 'install', - section, - name: selectedPackage.name, - currentRange: typeof currentRange === 'string' ? currentRange : null, - nextRange, - }) - } - - return updates -} - -export const promptForInstallSelection = async (packageJson) => { - if (!process.stdin.isTTY || !process.stdout.isTTY) { - throw new Error('Interactive add mode requires a TTY. Use --packages to select packages explicitly.') - } - - console.log('Выберите пакеты для установки в текущий package.json:') - for (const [index, selectedPackage] of INSTALLABLE_PACKAGES.entries()) { - const currentSection = findDependencySection(packageJson, selectedPackage.name) - const installedHint = currentSection ? ` Уже есть в ${currentSection}.` : '' - - console.log(` ${index + 1}. ${selectedPackage.name} (${selectedPackage.id})`) - console.log(` ${selectedPackage.description} Раздел по умолчанию: ${selectedPackage.section}.${installedHint}`) - } - - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }) - - try { - while (true) { - const answer = await readline.question( - 'Введите номера, ids или имена пакетов через запятую (например: 1,3 или components,types): ' - ) - - const tokens = parsePackageList(answer) - if (tokens.length === 0) { - return [] - } - - try { - return resolveInstallPackages(tokens) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - console.error(message) - } - } - } finally { - readline.close() - } -} - -const readPackageJson = (packageJsonPath) => { - const source = fs.readFileSync(packageJsonPath, 'utf8') - - return { - formatting: detectFormatting(source), - packageJson: JSON.parse(source), - } -} - -const writePackageJson = (packageJsonPath, packageJson, formatting) => { - fs.writeFileSync(packageJsonPath, serializePackageJson(packageJson, formatting), 'utf8') -} - -const printChanges = (changes) => { - for (const change of changes) { - const prefix = change.type === 'install' - ? `${change.section}: ${change.name} -> ${change.nextRange}` - : `${change.section}: ${change.name} ${change.currentRange} -> ${change.nextRange}` - - console.log(` ${prefix}`) - } -} - -export const runUpdate = (options) => { - const version = options.version ?? resolveLatestVersion() - const packageJsonPaths = collectPackageJsonPaths(options.target) - const reports = [] - - for (const packageJsonPath of packageJsonPaths) { - const { formatting, packageJson } = readPackageJson(packageJsonPath) - const updates = updatePackageJson(packageJson, version, options.exact) - - if (updates.length === 0) { - continue - } - - if (!options.dryRun) { - writePackageJson(packageJsonPath, packageJson, formatting) - } - - reports.push({ packageJsonPath, updates }) - } - - if (reports.length === 0) { - console.log(`No ${ROOT_PACKAGE}* dependencies found or changed under ${path.resolve(options.target)}`) - return - } - - const totalUpdates = reports.reduce((sum, report) => sum + report.updates.length, 0) - - console.log(`Resolved version: ${version}`) - for (const report of reports) { - console.log(report.packageJsonPath) - printChanges(report.updates) - } - - if (options.dryRun) { - console.log('Dry run enabled, package.json files were not modified') - return - } - - console.log( - `Updated ${totalUpdates} dependency entries in ${reports.length} package.json file(s) under ${path.resolve(options.target)}` - ) -} - -export const runAdd = async (options) => { - const version = options.version ?? resolveLatestVersion() - const packageJsonPath = resolvePackageJsonPath(path.resolve(options.target)) - const { formatting, packageJson } = readPackageJson(packageJsonPath) - const selectedPackages = options.packages - ? resolveInstallPackages(options.packages) - : await promptForInstallSelection(packageJson) - - if (selectedPackages.length === 0) { - console.log('Nothing selected, package.json was not modified') - return - } - - const updates = installPackages(packageJson, selectedPackages, version, options.exact) - - if (updates.length === 0) { - console.log(`Selected packages are already installed with matching ranges in ${packageJsonPath}`) - return - } - - console.log(`Resolved version: ${version}`) - console.log(packageJsonPath) - printChanges(updates) - - if (options.dryRun) { - console.log('Dry run enabled, package.json was not modified') - return - } - - writePackageJson(packageJsonPath, packageJson, formatting) - console.log(`Installed ${updates.length} package entries in ${packageJsonPath}`) -} - -export const main = async (argv = process.argv.slice(2)) => { - const options = parseArgs(argv) - - if (options.add) { - await runAdd(options) - return - } - - runUpdate(options) -} - -const isExecutedDirectly = () => { - const entryPath = process.argv[1] - - if (!entryPath) { - return false - } - - return pathToFileURL(path.resolve(entryPath)).href === import.meta.url -} - -if (isExecutedDirectly()) { - try { - await main() - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - console.error(message) - process.exit(1) - } -} diff --git a/eslint.config.js b/eslint.config.js index 43f2e950..8c0fbb39 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -172,5 +172,7 @@ export default defineConfig([ }, { ignores: ['dist/*'] }, { ignores: ['**/dist/*'] }, + { ignores: ['bin/embed-ui.mjs'] }, + { ignores: ['src/cmd/embed-ui/templates/**'] }, { ignores: ['packages/**/generated/**'] }, ]) diff --git a/meta/index.ts b/meta/index.ts index ba9dfb2e..1e07858f 100644 --- a/meta/index.ts +++ b/meta/index.ts @@ -1,608 +1,27 @@ -import type { SchemaListByTarget } from '~types/widget' import type { TranslationList } from '@retailcrm/embed-ui-v1-types/doc' -import type { UnionToArray } from '@retailcrm/embed-ui-v1-types/scaffolding' -import { keysOf } from '@/utilities' - -export const targetListDocumentation: { - [Target in keyof SchemaListByTarget]: { - description: TranslationList; - location: TranslationList; - contexts: UnionToArray; - customContexts: string[]; - actions: string[]; - } -} = { - 'customer/card:phone': { - description: { - 'en-GB': 'Widget for customer phone list item', - 'es-ES': 'Widget para el elemento de la lista de teléfonos del cliente', - 'ru-RU': 'Виджет для элемента списка телефонов клиента', - }, - location: { - 'en-GB': 'Right after the phone number in the list', - 'es-ES': 'Justo después del número de teléfono en la lista', - 'ru-RU': 'Сразу после номера телефона в списке', - }, - contexts: [ - 'customer/card', - 'customer/card:phone', - 'user/current', - 'settings', - ], - customContexts: ['customer'], - actions: [], - }, - 'customer/card:communications.after': { - description: { - 'en-GB': 'Widget for enhancing the communication section in the left column of the customer page.', - 'es-ES': 'Widget para ampliar la sección de comunicación en la columna izquierda de la página del cliente.', - 'ru-RU': 'Виджет для дополнения секции коммуникаций в левой колонке страницы клиента.', - }, - location: { - 'en-GB': 'Right below the short summary, in the communication section', - 'es-ES': 'Justo debajo del resumen breve, en la sección de comunicación', - 'ru-RU': 'Сразу под краткой сводкой, в секции коммуникаций', - }, - contexts: [ - 'customer/card', - 'user/current', - 'settings', - ], - customContexts: ['customer'], - actions: [], - }, - 'customer/card:inWork.before': { - description: { - 'en-GB': 'Widget for the contact request item', - 'es-ES': 'Widget para el elemento de solicitud de contacto', - 'ru-RU': 'Виджет для элемента обращений', - }, - location: { - 'en-GB': 'At the beginning of the "In Progress" block in the client card', - 'es-ES': 'Al principio del bloque "En progreso" en la tarjeta del cliente', - 'ru-RU': 'В начале блока "В работе" в карточке клиента', - }, - contexts: [ - 'customer/card', - 'user/current', - 'settings', - ], - customContexts: ['customer'], - actions: [], - }, - 'customer/card:inWork.after': { - description: { - 'en-GB': 'Widget for the contact request item', - 'es-ES': 'Widget para el elemento de solicitud de contacto', - 'ru-RU': 'Виджет для элемента обращений', - }, - location: { - 'en-GB': 'At the end of the "In progress" block in the client card', - 'es-ES': 'Al final del bloque "En progreso" en la tarjeta del cliente', - 'ru-RU': 'В конце блока "В работе" в карточке клиента', - }, - contexts: [ - 'customer/card', - 'user/current', - 'settings', - ], - customContexts: ['customer'], - actions: [], - }, - 'order/card:common.before': { - description: { - 'en-GB': 'Widget for the section with common data', - 'es-ES': 'Widget para la sección con datos comunes', - 'ru-RU': 'Виджет для секции с основными данными', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:common.after': { - description: { - 'en-GB': 'Widget for the section with common data', - 'es-ES': 'Widget para la sección con datos comunes', - 'ru-RU': 'Виджет для секции с основными данными', - }, - location: { - 'en-GB': 'Section end, right under the input fields', - 'es-ES': 'Fin de la sección, justo debajo de los campos de entrada', - 'ru-RU': 'Конец секции, под полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:customer.before': { - description: { - 'en-GB': 'Widget for the section with customer data', - 'es-ES': 'Widget para la sección con datos del cliente', - 'ru-RU': 'Виджет для секции с данными клиента', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:customer.after': { - description: { - 'en-GB': 'Widget for the section with customer data', - 'es-ES': 'Widget para la sección con datos del cliente', - 'ru-RU': 'Виджет для секции с данными клиента', - }, - location: { - 'en-GB': 'Section end, right under the input fields', - 'es-ES': 'Fin de la sección, justo debajo de los campos de entrada', - 'ru-RU': 'Конец секции, под полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:customer.email': { - description: { - 'en-GB': 'Widget for customer email input field', - 'es-ES': 'Widget para el campo de entrada del correo electrónico del cliente', - 'ru-RU': 'Виджет для поля ввода email клиента', - }, - location: { - 'en-GB': 'Right after the input field', - 'es-ES': 'Justo después del campo de entrada', - 'ru-RU': 'Сразу после поля ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:customer.phone': { - description: { - 'en-GB': 'Widget for customer phone input field', - 'es-ES': 'Widget para el campo de entrada del teléfono del cliente', - 'ru-RU': 'Виджет для поля ввода телефона клиента', - }, - location: { - 'en-GB': 'Right after the input field', - 'es-ES': 'Justo después del campo de entrada', - 'ru-RU': 'Сразу после поля ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:list.before': { - description: { - 'en-GB': 'Widget for the list of ordered items', - 'es-ES': 'Widget para la lista de artículos pedidos', - 'ru-RU': 'Виджет для списка позиций заказа', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:list.after': { - description: { - 'en-GB': 'Widget for the list of ordered items', - 'es-ES': 'Widget para la lista de artículos pedidos', - 'ru-RU': 'Виджет для списка позиций заказа', - }, - location: { - 'en-GB': 'Section start, right under the list', - 'es-ES': 'Inicio de la sección, justo debajo de la lista', - 'ru-RU': 'Начало секции, под списком', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:store.before': { - description: { - 'en-GB': 'Widget for the section with warehouse data', - 'es-ES': 'Widget para la sección con datos del almacén', - 'ru-RU': 'Виджет для секции с данными склада', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:dimensions.before': { - description: { - 'en-GB': 'Widget for the section with dimensions and weight', - 'es-ES': 'Widget para la sección con dimensiones y peso', - 'ru-RU': 'Виджет для секции с данными габаритов и веса', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:delivery.before': { - description: { - 'en-GB': 'Widget for the section with delivery data', - 'es-ES': 'Widget para la sección con datos de entrega', - 'ru-RU': 'Виджет для секции с данными доставки', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:delivery.after': { - description: { - 'en-GB': 'Widget for the section with delivery data', - 'es-ES': 'Widget para la sección con datos de entrega', - 'ru-RU': 'Виджет для секции с данными доставки', - }, - location: { - 'en-GB': 'Section end, right under the input fields', - 'es-ES': 'Fin de la sección, justo debajo de los campos de entrada', - 'ru-RU': 'Конец секции, под полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:delivery.address': { - description: { - 'en-GB': 'Widget for delivery address input field', - 'es-ES': 'Widget para el campo de entrada de la dirección de entrega', - 'ru-RU': 'Виджет для поля ввода адреса доставки', - }, - location: { - 'en-GB': 'Right under the input field', - 'es-ES': 'Justo debajo del campo de entrada', - 'ru-RU': 'Под полем ввода адреса', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:payment.before': { - description: { - 'en-GB': 'Widget for the section with payment data', - 'es-ES': 'Widget para la sección con datos de pago', - 'ru-RU': 'Виджет для секции с данными по оплате', - }, - location: { - 'en-GB': 'Section start, right above the input fields', - 'es-ES': 'Inicio de la sección, justo encima de los campos de entrada', - 'ru-RU': 'Начало секции, над полями ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/card:comment.manager.before': { - description: { - 'en-GB': 'Widget for the block "Manager comment"', - 'es-ES': 'Widget para el bloque "Comentario del asesor"', - 'ru-RU': 'Виджет для блока "Комментарии оператора"', - }, - location: { - 'en-GB': 'Section start, right above the input field', - 'es-ES': 'Inicio de la sección, justo encima del campo de entrada', - 'ru-RU': 'Начало секции, над полем ввода', - }, - contexts: [ - 'order/card', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/mg:list.before': { - description: { - 'en-GB': 'Widget for the block "Order items"', - 'es-ES': 'Widget para el bloque "Artículos del pedido"', - 'ru-RU': 'Виджет для блока "Состав заказа"', - }, - location: { - 'en-GB': 'Section start, right above the list of order items', - 'es-ES': 'Inicio de la sección, justo encima de la lista de artículos del pedido', - 'ru-RU': 'Начало секции, над списком товарных позиций', - }, - contexts: [ - 'order/card', - 'order/card:settings', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/mg:list.after': { - description: { - 'en-GB': 'Widget for the block "Order items"', - 'es-ES': 'Widget para el bloque "Artículos del pedido"', - 'ru-RU': 'Виджет для блока "Состав заказа"', - }, - location: { - 'en-GB': 'Section end, right after the list of order items and before the discount, privilege selection, etc. input fields', - 'es-ES': 'Fin de la sección, justo después de la lista de artículos del pedido y antes de los campos de entrada de descuento, selección de privilegios, etc.', - 'ru-RU': 'Конец секции, сразу после списка товарных позиций и до полей ввода скидки, выбора привилегии и т.п.', - }, - contexts: [ - 'order/card', - 'order/card:settings', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/mg:delivery.before': { - description: { - 'en-GB': 'Widget for the block "Delivery"', - 'es-ES': 'Widget para el bloque "Entrega"', - 'ru-RU': 'Виджет для блока "Доставка"', - }, - location: { - 'en-GB': 'Section start, right above the input field', - 'es-ES': 'Inicio de la sección, justo encima del campo de entrada', - 'ru-RU': 'Начало секции, над полем ввода', - }, - contexts: [ - 'order/card', - 'order/card:settings', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/mg:delivery.after': { - description: { - 'en-GB': 'Widget for the block "Delivery"', - 'es-ES': 'Widget para el bloque "Entrega"', - 'ru-RU': 'Виджет для блока "Доставка"', - }, - location: { - 'en-GB': 'Section end, right under the input fields', - 'es-ES': 'Fin de la sección, justo debajo de los campos de entrada', - 'ru-RU': 'Конец секции, под полями ввода', - }, - contexts: [ - 'order/card', - 'order/card:settings', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/mg:payment.before': { - description: { - 'en-GB': 'Widget for the block "Payment"', - 'es-ES': 'Widget para el bloque "Pago"', - 'ru-RU': 'Виджет для блока "Оплата"', - }, - location: { - 'en-GB': 'Section start, right above the input field', - 'es-ES': 'Inicio de la sección, justo encima del campo de entrada', - 'ru-RU': 'Начало секции, над полем ввода', - }, - contexts: [ - 'order/card', - 'order/card:settings', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, - 'order/mg:payment.after': { - description: { - 'en-GB': 'Widget for the block "Payment"', - 'es-ES': 'Widget para el bloque "Pago"', - 'ru-RU': 'Виджет для блока "Оплата"', - }, - location: { - 'en-GB': 'Section end, after the list of payments, controls, and custom fields', - 'es-ES': 'Fin de la sección, después de la lista de pagos, controles y campos personalizados', - 'ru-RU': 'Конец секции, после списка оплат, контролов и пользовательских полей', - }, - contexts: [ - 'order/card', - 'order/card:settings', - 'user/current', - 'settings', - ], - customContexts: [ - 'order', - ], - actions: [ - 'order/card', - ], - }, -} - -export const pageListDocumentation = [{ +export const pageListDocumentation: Array<{ + id: string; + description: TranslationList; +}> = [{ id: 'customer/card', description: { 'en-GB': 'Customer page', 'es-ES': 'Página del cliente', 'ru-RU': 'Страница клиента', - } as TranslationList, - targets: keysOf(targetListDocumentation).filter(target => target.startsWith('customer/card:')), + }, }, { id: 'order/card', description: { 'en-GB': 'Page with the order creation/editing form', 'es-ES': 'Página con el formulario de creación/edición de pedidos', 'ru-RU': 'Страница с формой создания/редактирования заказа', - } as TranslationList, - targets: keysOf(targetListDocumentation).filter(target => target.startsWith('order/card:')), + }, }, { id: 'order/mg', description: { 'en-GB': 'Page with the order creation/editing form in chats', 'es-ES': 'Página con el formulario de creación/edición de pedidos en los chats', 'ru-RU': 'Страница с формой создания/редактирования заказа в чатах', - } as TranslationList, - targets: keysOf(targetListDocumentation).filter(target => target.startsWith('order/mg:')), + }, }] diff --git a/package.json b/package.json index cb09e031..0dfc4510 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,25 @@ "./dist/*": "./dist/*", "./types/*": "./types/*" }, - "bin": "./bin/embed-ui-update.mjs", + "bin": "./bin/embed-ui.mjs", + "files": [ + "bin", + "dist", + "index.d.ts", + "types", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], "types": "index.d.ts", "workspaces": [ "packages/*" ], "scripts": { "build": "yarn build:code && yarn build:meta", - "build:code": "vite build", + "build:code": "yarn build:lib && yarn build:bin", + "build:lib": "vite build", + "build:bin": "vite build -c vite.config.bin.ts", "build:meta": "npx tsx scripts/build.meta.ts", "eslint": "eslint .", "test": "vitest --run", @@ -44,7 +55,9 @@ "@remote-ui/rpc": "^1.4.5", "@retailcrm/embed-ui-v1-components": "^0.9.21", "@retailcrm/embed-ui-v1-contexts": "^0.9.21", - "@retailcrm/embed-ui-v1-types": "^0.9.21" + "@retailcrm/embed-ui-v1-endpoint": "^0.9.21", + "@retailcrm/embed-ui-v1-types": "^0.9.21", + "yargs": "^17.7.2" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", @@ -84,8 +97,7 @@ "vite-plugin-dts": "^4.5.4", "vitest": "^4.1.3", "vue": "^3.5.32", - "vue-eslint-parser": "^10.4.0", - "yargs": "^17.7.2" + "vue-eslint-parser": "^10.4.0" }, "resolutions": { "handlebars": "^4.7.9" diff --git a/packages/v1-components/docs/profiles/pages/ModalSidebar.yml b/packages/v1-components/docs/profiles/pages/ModalSidebar.yml index 75513429..598b111f 100644 --- a/packages/v1-components/docs/profiles/pages/ModalSidebar.yml +++ b/packages/v1-components/docs/profiles/pages/ModalSidebar.yml @@ -36,7 +36,7 @@ examples: