diff --git a/.cursor/rules/agent-workflow.mdc b/.cursor/rules/agent-workflow.mdc new file mode 100644 index 0000000..4b2c4a2 --- /dev/null +++ b/.cursor/rules/agent-workflow.mdc @@ -0,0 +1,13 @@ +--- +description: Правила работы агента в репозитории OPM +alwaysApply: true +--- + +# Правила работы агента в этом репо + +- Меняй только то, что нужно для задачи; без «заодно»-рефакторинга и лишних markdown-файлов. +- Сохраняй русские имена и стиль соседнего кода. +- После существенных правок ядра/CLI — запускай релевантные тесты (`tasks/test.os` или узкий сценарий). +- Коммиты и push — только по явной просьбе пользователя. +- Не коммить секреты (`GITHUB_OAUTH_TOKEN`, `opm.cfg` с токенами/прокси). +- Пользовательский `README.md` обновляй, если меняется публичное поведение CLI. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6cb7b9a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,233 @@ +# AGENTS.md — руководство для AI-агентов (OPM) + +Документ для агентов, работающих с этим репозиторием. Цель — быстро понять архитектуру, где править код и как проверять изменения. + +## Что это за проект + +**OPM (OneScript Package Manager)** — менеджер пакетов для [OneScript](https://oscript.io): сборка `.ospx`, установка из хаба/файла/URL, разрешение зависимостей, публикация, scaffold и запуск задач. + +- Репозиторий: https://github.com/oscript-library/opm +- Лицензия: Apache-2.0 +- Версия продукта: `КонстантыOpm.ВерсияПродукта` (сейчас `1.1.2`) +- Требуемая среда: OneScript ≥ **1.8.3** (`packagedef`) +- Хабы: `http://hub.oscript.io`, запасной `http://hub.oscript.ru` +- Packaging docs: https://hub.oscript.io/packaging + +## Стек + +| Слой | Технология | +|------|------------| +| Язык | OneScript / BSL (`.os`), **русские** идентификаторы | +| CLI | пакет `cli` | +| Логи | `logos` (`oscript.app.opm`) | +| Unit | `1testrunner` | +| BDD | `1bdd` (Gherkin на русском) | +| Coverage | `coverage` + `oscript -codestat=` | +| CI | GitHub Actions + SonarQube (`sonar.openbsl.ru`) | + +Runtime-зависимости — в корневом `packagedef`: `fs`, `asserts`, `fluent`, `logos`, `cli`, `tempfiles`, `gitrunner`, `reflector`. + +## Структура репозитория + +``` +opm/ +├── packagedef # манифест пакета OPM +├── src/ +│ ├── cmd/ +│ │ ├── opm.os # точка входа CLI +│ │ ├── Классы/ # КомандаOpm_*.os, ИсполнительЗадач +│ │ └── Модули/ # ПараметрыПриложенияOpm +│ └── core/ +│ ├── Классы/ # менеджеры, сборщик, установка... +│ └── Модули/ # КонстантыOpm, РаботаС*, НастройкиOpm +├── tasks/ # opm run / opm test +│ ├── test.os # unit + bdd +│ ├── coverage.os # как в CI +│ └── oscript.cfg +├── tests/ # unit-тесты (1testrunner) +├── features/ # BDD (1bdd) + step_definitions/ +├── .github/workflows/ # CI / release / rebase +└── oscript_modules/ # локальные зависимости (vendor, в .gitignore) +``` + +Отдельного каталога `docs/` нет — ориентир: `README.md` и этот файл. + +## Архитектура (слои) + +``` +src/cmd/opm.os (cli.КонсольноеПриложение) + → КомандаOpm_* (ОписаниеКоманды / ВыполнитьКоманду) + → РаботаСПакетами / СборщикПакета / ИсполнительЗадач / ... + → МенеджерУстановкиПакетов / МенеджерПолученияПакетов / УстановкаПакета / ... +``` + +### Карта «хочу изменить X» + +| Задача | Куда смотреть | +|--------|----------------| +| CLI-команда / флаги | `src/cmd/opm.os`, `src/cmd/Классы/КомандаOpm_*.os` | +| Install / зависимости | `РаботаСПакетами`, `МенеджерУстановкиПакетов`, `УстановкаПакета`, `КэшУстановленныхПакетов` | +| Скачивание с хаба | `МенеджерПолученияПакетов`, `СерверПакетов`, `КонстантыOpm` | +| Сборка `.ospx` | `СборщикПакета`, `ОписаниеПакета`, `СериализацияМетаданныхПакета` | +| Publish | `КомандаOpm_Push` | +| Версии `Имя@Версия` | `РаботаСВерсиями` | +| Чтение `packagedef` | `РаботаСОписаниемПакета`, `ОписаниеПакета` | +| `opm.cfg` / прокси / зеркала | `ПараметрыПриложенияOpm`, `НастройкиOpm` | +| Версия продукта | `src/core/Модули/КонстантыOpm.os` (+ fallback в `packagedef`) | + +### Команды CLI + +| Команда | Класс | +|---------|-------| +| `a app` | `КомандаOpm_App` | +| `b build` | `КомандаOpm_Build` | +| `c config` | `КомандаOpm_Config` | +| `i install` | `КомандаOpm_Install` | +| `ls list` | `КомандаOpm_List` | +| `pre prepare` | `КомандаOpm_Prepare` | +| `p push` | `КомандаOpm_Push` | +| `r run` | `КомандаOpm_Run` | +| `test` | `КомандаOpm_Test` | +| `u update` | `КомандаOpm_Update` | +| `version` | `КомандаOpm_Version` | + +### Потоки данных (кратко) + +**Install:** CLI → `РаботаСПакетами` → download (`МенеджерПолученияПакетов`) → `УстановкаПакета` (unzip `.ospx`) → рекурсивные зависимости → кэш установленных. + +**Build:** `СборщикПакета` читает `packagedef` (контекст `Описание` = fluent `ОписаниеПакета`) → hooks → `{Имя}-{Версия}.ospx` = ZIP(`opm-metadata.xml` + `content.zip`). + +**Режимы установки:** локально → `./oscript_modules`; глобально → системный `lib` OneScript (`РежимУстановкиПакетов`). + +## Окружение и команды + +### Подготовка + +```powershell +# Нужен OneScript ≥ 1.8.3 (stable или 1.8.4 как в CI) +opm install opm +opm install 1testrunner +opm install 1bdd +opm install coverage +opm install -l --dev +``` + +### Запуск из исходников + +```powershell +oscript src\cmd\opm.os --help +oscript src\cmd\opm.os version +oscript src\cmd\opm.os install --local +oscript src\cmd\opm.os build --mf .\packagedef . +``` + +Отладка: `.vscode/launch.json`, `LOGOS_CONFIG=logger.oscript.app.opm=DEBUG`. + +### Тесты + +```powershell +oscript tasks\test.os # unit + bdd +oscript tasks\coverage.os # как в CI +opm test # через CLI +``` + +Отчёты: каталог `out/`. + +### Типовые CLI-вызовы + +```powershell +opm install asserts +opm install --local # зависимости packagedef → ./oscript_modules +opm install --local --dev +opm install -f my.ospx --local +opm install Package@1.2.0 +opm build --mf .\packagedef . +opm list +opm list --remote +opm prepare my-package +opm update opm +``` + +Полезные переменные: `OSCRIPTBIN`, `OPM_HUB_MIRROR`, `OPM_HUB_CHANNEL`, `GITHUB_OAUTH_TOKEN`, `LOGOS_CONFIG`. + +## Соглашения по коду + +1. **Русский BSL** — имена процедур, переменных, каталогов `Классы/` / `Модули/`. +2. Пользовательские строки — через `НСтр("ru='...';en='...')`. +3. Модули = статический API (`РаботаС*`, `КонстантыOpm`); классы = состояние (`Менеджер*`, `УстановкаПакета`). +4. Подключения: `#Использовать logos`, `#Использовать "../core"`, `#Использовать cli`. +5. Публичный API пакета задаётся в `packagedef` через `.ОпределяетКласс` / `.ОпределяетМодуль`. +6. OneScript подхватывает классы/модули по имени файла из `Классы/` и `Модули/`. +7. Комментарии — на русском. Не рефакторить стиль «заодно», если задача этого не требует. + +### Добавление CLI-команды + +1. Создать `src/cmd/Классы/КомандаOpm_Foo.os` по образцу `КомандаOpm_Build.os`: + - `ОписаниеКоманды(КомандаПриложения)` — опции и аргументы; + - `ВыполнитьКоманду(КомандаПриложения)` — логика. +2. Зарегистрировать в `src/cmd/opm.os`: + `Приложение.ДобавитьКоманду("f foo", НСтр(...), Новый КомандаOpm_Foo);` +3. При необходимости — unit/BDD и строка в `README.md`. + +**Не копировать** `ШаблонКоманды.os-template` — там устаревший cmdline API. Актуальный паттерн — пакет `cli`, как в `КомандаOpm_Build.os`. + +### Bump версии OPM + +1. `ВерсияПродукта` в `src/core/Модули/КонстантыOpm.os` +2. Fallback-строка в `packagedef` (`Иначе ВерсияПродукта = "..."`) +3. Сборка / тесты / release workflow + +### Зависимости самого OPM + +В корневом `packagedef`: + +```bsl +.ЗависитОт("имя", "min.version") +.РазработкаЗависитОт("имя", "min.version") +``` + +Затем `opm install -l` / `opm install -l --dev`. + +## Тесты: контракт + +**Unit** (`tests/*.os`, 1testrunner): + +- `ПолучитьСписокТестов(Тестирование)` +- `ПередЗапускомТеста` / `ПослеЗапускаТеста` +- методы `ТестДолжен_*` +- asserts: `#Использовать asserts`, `Ожидаем` + +Файлы: `packagedef-test.os`, `versions-test.os`, `mft-serializer-test.os`, `pkg-cache.os`, `packagelist.os`, `download.os`, `build-install-test.os`. + +**BDD** (`features/*.feature` + `features/step_definitions/*.os`, `# language: ru`): + +- `opm-build.feature`, `install-file.feature`, `Настройки.feature` + +При правках логики — добавляй/обновляй ближайший тест в той же области (см. карту выше). + +## CI + +`.github/workflows/main.yml`: + +- матрица: ubuntu / windows / macos × oscript `stable` и `1.8.4` +- установка deps → `oscript ./tasks/coverage.os` +- Sonar только на `ubuntu-latest` + `stable` + +Release: `.github/workflows/release.yml` (маска `opm-*.ospx`). + +Перед PR желательно прогнать `oscript tasks\test.os` (или `coverage.os`). + +## Критичные ограничения и ловушки + +1. **CLI ≥ 0.15:** опции **до** аргументов. Правильно: `opm build --mf ./packagedef .`. Неправильно: `opm build . -mf ...`. +2. **Версия продукта** дублируется: `КонстантыOpm` и fallback в `packagedef` — менять согласованно. +3. Формат `.ospx`: внешний ZIP → `opm-metadata.xml` + `content.zip`; не путать уровни. +4. Имена пакетов на хабе **регистрозависимы** при скачивании, сравнение имён — нет. +5. Разрешение зависимостей слабое: для уже установленного пакета фактически проверяется min-версия; max используется ограниченно. +6. Локальный `-dest` игнорируется при `--local`. +7. Канал `push` `auto` только из git-ветки **`master`** (не `main`). +8. Hooks манифеста (`ПередСборкой` / `ПриСборке` / `ПослеСборки`, `ПередУстановкой` / `ПриУстановке`) вызываются через рефлектор — не ломать сигнатуры. +9. На Linux пользовательский конфиг — **`.opm.cfg`** (с точкой); на Windows — `opm.cfg` в `%USERPROFILE%`. Приоритет: cwd → user → system → каталог opm. +10. Хабы по умолчанию — **http**, не https. +11. `oscript_modules` в `.gitignore`, но в `packagedef` есть `.ВключитьФайл("oscript_modules")` (бандл в дистрибутив). +12. Кириллические пути (`Классы`, `Модули`): на Windows учитывать кодировку консоли при запуске задач. \ No newline at end of file diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index feda711..df03357 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -25,7 +25,7 @@ .Перечисление("stable", "stable", "Канал содержащий стабильные версии пакетов") .Перечисление("dev", "dev" , "Канал содержащий разработческие версии пакетов") .ВОкружении("OPM_HUB_CHANNEL"); - ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "СерверУдаленногоХранилища", "Имя сервера для публикации. + ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "ОсновнойСерверПакетов", "Имя сервера для публикации. | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'.") .ВОкружении("OPM_HUB_MIRROR") .ТПеречисление(); @@ -135,6 +135,12 @@ КонецФункции +Функция ПортПоУмолчанию(Знач Сервер) + + Возврат ?(СтрНачинаетсяС(НРег(Сервер), "https://"), 443, 80); + +КонецФункции + Процедура ОтправитьПакетВХаб(Знач ТокенАвторизации, Знач ФайлПакета, Знач Канал, Знач ИмяСервераПакетов) ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ФайлПакета.ПолноеИмя); @@ -142,15 +148,16 @@ ДоступныеСервераПакетов = НастройкиOpm.ПолучитьНастройки().СервераПакетов; - // Для настроек по умолчанию Сервер = КонстантыOpm.СерверУдаленногоХранилища; Ресурс = КонстантыOpm.РесурсПубликацииПакетов; + Порт = Неопределено; Для Каждого НастройкаСервера Из ДоступныеСервераПакетов Цикл Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда Сервер = НастройкаСервера.Сервер; Ресурс = НастройкаСервера.РесурсПубликацииПакетов; + Порт = НастройкаСервера.Порт; Прервать; КонецЕсли; @@ -164,7 +171,14 @@ Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); Заголовки.Вставить("CHANNEL", Канал); - Соединение = Новый HTTPСоединение(Сервер); + Порт = ?(Порт = Неопределено, ПортПоУмолчанию(Сервер), Порт); + Настройки = НастройкиOpm.ПолучитьНастройки(); + Если Настройки.ИспользоватьПрокси Тогда + НастройкиПрокси = НастройкиOpm.ПолучитьИнтернетПрокси(); + Соединение = Новый HTTPСоединение(Сервер, Порт, , , НастройкиПрокси); + Иначе + Соединение = Новый HTTPСоединение(Сервер, Порт); + КонецЕсли; Запрос = Новый HTTPЗапрос(Ресурс, Заголовки); Запрос.УстановитьТелоИзДвоичныхДанных(ДвоичныеДанныеФайла); diff --git "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" index 1a441ad..fe709e4 100644 --- "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" +++ "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" @@ -59,7 +59,12 @@ Для каждого ТекущийСерверПакетов Из СервераПакетов Цикл Сервер = ПолучитьЗначение(ТекущийСерверПакетов, "Сервер", ""); - Порт = Число(ПолучитьЗначение(ТекущийСерверПакетов, "Порт", 80)); + // Порт 80 не является корректным дефолтом и зависит от протокола. + // поэтому если в конфиге ничего не указано, то определение порта уходит на прикладной уровень и дефолты HTTPСоединение + Порт = ПолучитьЗначение(ТекущийСерверПакетов, "Порт", Неопределено); + Если Не Порт = Неопределено Тогда + Порт = Число(Порт); + КонецЕсли; ПутьНаСервере = ПолучитьЗначение(ТекущийСерверПакетов, "ПутьНаСервере", "/"); Имя = ПолучитьЗначение(ТекущийСерверПакетов, "Имя", СтрШаблон("ДопСервер_%1", Индекс)); РесурсПубликацииПакетов = ПолучитьЗначение(ТекущийСерверПакетов, "РесурсПубликацииПакетов", "/"); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" index 40f745c..8dc7542 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" @@ -103,7 +103,7 @@ Знач Сервер, Знач ПутьНаСервере = "", Знач РесурсПубликацииПакетов = "", - Знач Порт = 80, + Знач Порт = Неопределено, Знач Приоритет = Неопределено) Экспорт мНастройки.СервераПакетов.Добавить(НастройкиСервераПакетов(Имя, Сервер, ПутьНаСервере, РесурсПубликацииПакетов, Порт, Приоритет)); @@ -121,8 +121,8 @@ Процедура ИнициализацияСерверовПакетов() // Сервера пакетов по умолчанию - ДобавитьСерверПакетов("ОсновнойСерверПакетов", КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.ПутьВХранилище, КонстантыOpm.РесурсПубликацииПакетов, 80, 0); - ДобавитьСерверПакетов("ЗапаснойСерверПакетов", КонстантыOpm.СерверЗапасногоХранилища, КонстантыOpm.ПутьВЗапасномХранилище, Неопределено, 80, 1); + ДобавитьСерверПакетов("ОсновнойСерверПакетов", КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.ПутьВХранилище, КонстантыOpm.РесурсПубликацииПакетов, Неопределено, 0); + ДобавитьСерверПакетов("ЗапаснойСерверПакетов", КонстантыOpm.СерверЗапасногоХранилища, КонстантыOpm.ПутьВЗапасномХранилище, Неопределено, Неопределено, 1); КонецПроцедуры Процедура Инициализация()