Локальная разработка
Структура репо
gitflic-cli/
├── bin/gitflic # bash-шим → node lib/gitflic.mjs
├── lib/
│ ├── gitflic.mjs # entry: dispatcher
│ ├── http.mjs # fetch + retry + parseArgs + resolveToken
│ ├── secret.mjs # OS keychain / libsecret / chmod 0600
│ ├── config.mjs # persistent config (chmod 0600)
│ ├── cache.mjs # file-based project-UUID cache
│ ├── paginate.mjs # --all auto-pagination
│ ├── format.mjs # вывод + цвета
│ ├── tui.mjs # интерактивный браузер MR
│ └── cmd/ # один файл = один модуль
├── mcp-server/ # MCP-сервер для AI-агентов
├── docs/ # эта документация (VitePress)
├── scripts/ # утилиты (docs dev-server)
├── install.sh / install.ps1 # установщики
├── gitflic-ci.yaml # (удалён в 0.2.0 — собираем локально)
├── package.json # только devDeps (vitest)
└── vitest.config.mjsКонвенции
Стиль кода
- ESM only,
"type": "module"в package.json. - Node 18+ baseline (используем
globalThis.fetch,node:fs/promises, etc). node --check <file>для syntax — никакого TypeScript, никакого Babel, никакого bundler.- 2-space indent, no semicolons в конце statements… шучу, с semicolons. Single quotes для строк.
- Каждый модуль экспортирует
usage(строка) иhandle(ctx, sub, args, flags)— это контракт диспетчера.
Структура cmd-модуля
// lib/cmd/<name>.mjs
export const usage = `
<command> <subcommand> [--flag value]
One-line description.
`;
export async function handle(ctx, sub, args, flags) {
switch (sub) {
case "list": return handleList(ctx, args, flags);
case "create": return handleCreate(ctx, args, flags);
// ...
case undefined:
process.stdout.write(usage);
return;
default:
process.stderr.write(`gitflic ${name}: unknown subcommand: ${sub}\n`);
process.exit(2);
}
}Добавление новой команды
- Создай
lib/cmd/<name>.mjsсusageиhandle. - Импортируй в
lib/gitflic.mjsи добавь вCOMMANDS. - Если не требует
--project, добавь вGLOBALset. - Напиши тесты в
lib/__tests__/<name>.test.mjs(vitest). - Если добавляешь новый sensitive параметр — обнови
isSensitiveKey()вlib/secret.mjs. - Обнови README +
docs/commands/<name>.md.
Тесты
- Vitest, 163 теста, ~300ms на полный прогон.
- Тесты лежат рядом с кодом:
lib/__tests__/<name>.test.mjs. - Покрытие: все публичные функции, command handlers, edge cases (env vars, file modes, secret backend fallback).
- Не мокай global fetch для тестов auth.mjs — используй
vi.stubGlobal("fetch", mockFn).
npm test # один прогон
npm run test:watch # watch mode
npx vitest run path/to/specific.test.mjs # один файлЛинтинг
Сейчас нет линтера (только node --check). Если хочется eslint — отдельный PR. Минимальный sanity-check:
npm run lint
# или
node scripts/build.mjs --no-test --no-bundleДокументация
Docs лежат в docs/ — это VitePress сайт (Markdown + конфиг в docs/.vitepress/config.mjs).
npm run docs:dev # dev-server с HMR (по умолчанию :5173)
npm run docs:build # статическая сборка → docs/.vitepress/dist
npm run docs:preview # локальный предпросмотр собранного сайта
npm run docs:publish # build + пуш собранного сайта в GitHub-зеркало (деплой)- Страницы — обычный Markdown в
docs/**.md. Навигация/сайдбар — вdocs/.vitepress/config.mjs. - Статика (иконки, картинки) — в
docs/public/(раздаётся из корня сайта, напр./_media/icon.svg). - Тема/брендинг —
docs/.vitepress/theme/(GitFlic-красный). Поиск — встроенный локальный. - Важно: в Markdown вне код-блоков не пиши голые
<плейсхолдеры>— Vue-компилятор примет их за теги. Оборачивай в backticks:`<name>`.
Сборка и релиз
scripts/build.mjs — полная локальная сборка: syntax-check всех .mjs, JSON-валидация, vitest, tar.gz portable-бандла в dist/.
npm run build # lint + test + tar.gz
npm run build -- --no-test # быстрая итерация (без тестов)
npm run build -- --no-bundle # только проверки, без tar.gzscripts/release.mjs — бампит версию в package.json, пересобирает, копирует tar.gz в docs/public/downloads/ (раздаётся VitePress как /downloads/<file> — артефакты качаются прямо с сайта, без S3), и пересобирает там же index.html со списком релизов.
npm run release -- --version 0.3.0 # бамп + билд + ship
npm run release # интерактивно спросит версию
npm run docs:build # собрать VitePress перед деплоемПочему так: на публичном gitflic.ru нет shared runners (нужна Company + self-hosted runner.jar — это платный VPS), и покупать S3 ради одного tar.gz тоже не хочется. Поэтому:
- CI/CD выпилен (
gitflic-ci.yamlудалён). - Документация — статический VitePress сайт. Исходник (
docs/) живёт в этом репо; собранный сайт публикуется в GitHub-зеркалоzhimbura/gitflic-cli-docs(его корень = собранный сайт, без build-шага), а Timeweb Apps раздаёт зеркало статикой. Деплой одной командой:npm run docs:publish(=docs:build+ пушdistв зеркало, gh-pages style). - Артефакты релизов лежат в
docs/public/downloads/→ попадают в собранный сайт под тем же доменом (/downloads/).
Workflow релиза целиком:
# 1. Поправил код, прогнал тесты
npm run build
# 2. Бамп + ship tar.gz в docs/public/downloads/
npm run release -- --version 0.3.0
# 3. Коммит исходников (код + docs/) в основной репо
git add . && git commit -m "release: v0.3.0"
git push
# 4. Собрать и опубликовать доку в зеркало (Timeweb подхватит)
npm run docs:publishОтладка
# Verbose retry logs
GITFLIC_HTTP_RETRIES=0 GITFLIC_NO_RETRY=1 GITFLIC_HTTP_RETRY_DELAY=10 gitflic ...
# Без кеша
GITFLIC_NO_CACHE=1 gitflic ...
# Без config-файла (CI-mode)
GITFLIC_NO_CONFIG=1 gitflic ...
# Без secret store (только env var)
GITFLIC_NO_SECRET_STORE=1 gitflic ...
# Print resolved API calls
DEBUG=gitflic:* gitflic ... # пока не реализовано, но легко добавить