@rt-tools/agent-kit 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/assets/checks/board.github.mjs +48 -1
- package/assets/checks/check-board.github.mjs +84 -1
- package/assets/checks/check-lib-layers.mjs +13 -524
- package/assets/checks/check-specs.mjs +11 -782
- package/assets/checks/check-styles.mjs +185 -15
- package/assets/checks/lib-boundaries.mjs +143 -0
- package/assets/checks/lib-common.mjs +149 -0
- package/assets/checks/lib-domains.mjs +205 -0
- package/assets/checks/lib-manifests.mjs +60 -0
- package/assets/checks/lib-reexports.mjs +101 -0
- package/assets/checks/rt-kit-checks.config.mjs +17 -0
- package/assets/checks/spec-anchors.mjs +297 -0
- package/assets/checks/spec-common.mjs +222 -0
- package/assets/checks/spec-contract.mjs +152 -0
- package/assets/checks/spec-scenarios.mjs +201 -0
- package/assets/defaults/project.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -4
- package/assets/hooks/skill-gate.sh +1 -1
- package/assets/hooks/sql-guard-parse.sh +187 -0
- package/assets/hooks/sql-guard-request.sh +117 -0
- package/assets/hooks/sql-guard-target.sh +134 -0
- package/assets/hooks/sql-guard-write.sh +212 -0
- package/assets/hooks/sql-guard.sh +26 -596
- package/assets/hooks/waiting-turn-guard.sh +42 -13
- package/assets/laws/delivery.md +7 -0
- package/assets/laws/work-conduct.md +9 -0
- package/assets/patterns/admin-lists-screen.md +25 -14
- package/assets/patterns/admin-nav-item.md +1 -1
- package/assets/patterns/component-structure-new.md +1 -1
- package/assets/patterns/entity-aside.md +4 -2
- package/assets/patterns/observability-record.md +9 -0
- package/assets/patterns/shared-code-new.md +2 -2
- package/assets/patterns/task-flow-close.md +7 -1
- package/assets/rules/git-workflow.azure.md +7 -0
- package/assets/rules/git-workflow.github.md +41 -0
- package/assets/rules/git-workflow.gitlab.md +7 -0
- package/assets/rules/lib-layers.md +4 -0
- package/assets/rules/lists.md +10 -10
- package/assets/rules/shared-code.md +1 -1
- package/assets/rules/task-flow.md +47 -7
- package/assets/rules/testing.md +31 -0
- package/assets/rules/typescript-conventions.md +7 -0
- package/assets/skills/agent-kit.md +36 -0
- package/assets/templates/proposal.md +21 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +117 -87
- package/bin/agent-kit.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/argv.d.ts.map +1 -1
- package/lib/argv.js +6 -4
- package/lib/argv.js.map +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +2 -1
- package/lib/assets.js.map +1 -1
- package/lib/cargo.d.ts +20 -0
- package/lib/cargo.d.ts.map +1 -1
- package/lib/cargo.js.map +1 -1
- package/lib/cascade.d.ts +55 -0
- package/lib/cascade.d.ts.map +1 -0
- package/lib/cascade.js +131 -0
- package/lib/cascade.js.map +1 -0
- package/lib/catalog.d.ts +0 -75
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +44 -127
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +152 -85
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +5 -5
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +2 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +7 -5
- package/lib/config.js.map +1 -1
- package/lib/enroll.d.ts +64 -0
- package/lib/enroll.d.ts.map +1 -0
- package/lib/enroll.js +170 -0
- package/lib/enroll.js.map +1 -0
- package/lib/freshness.d.ts.map +1 -1
- package/lib/freshness.js +31 -17
- package/lib/freshness.js.map +1 -1
- package/lib/hooks-map.d.ts +30 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +80 -18
- package/lib/hooks-map.js.map +1 -1
- package/lib/integrity.d.ts +1 -2
- package/lib/integrity.d.ts.map +1 -1
- package/lib/integrity.js +0 -1
- package/lib/integrity.js.map +1 -1
- package/lib/observations.d.ts.map +1 -1
- package/lib/observations.js +25 -12
- package/lib/observations.js.map +1 -1
- package/lib/order.d.ts +10 -0
- package/lib/order.d.ts.map +1 -0
- package/lib/order.js +14 -0
- package/lib/order.js.map +1 -0
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +8 -2
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +1 -1
- package/lib/plan.js.map +1 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +25 -8
- package/lib/proposals.js.map +1 -1
- package/lib/sections.js +1 -1
- package/lib/sections.js.map +1 -1
- package/lib/ship.d.ts.map +1 -1
- package/lib/ship.js +9 -1
- package/lib/ship.js.map +1 -1
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.js +14 -10
- package/lib/shipment.js.map +1 -1
- package/lib/snapshot.d.ts.map +1 -1
- package/lib/snapshot.js +2 -1
- package/lib/snapshot.js.map +1 -1
- package/lib/stamp.js +1 -1
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +12 -2
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +11 -10
- package/lib/sync.js.map +1 -1
- package/lib/vars.d.ts.map +1 -1
- package/lib/vars.js +2 -3
- package/lib/vars.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.9.1.tgz +0 -0
- package/rt-tools-agent-kit-0.8.3.tgz +0 -0
package/lib/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Конфиг проекта — `.claude/rt-kit.json`.\n *\n * Всё, что пакет не может знать сам: значения для дырок, куда класть каждый род ресурса и от\n * чего проект отказался. Конфиг лежит в дереве проекта и коммитится: раскладка обязана\n * повторяться на чужой машине без вопросов, иначе разложенное расходится между разработчиками.\n */\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Род ресурса. Он же имя каталога в `assets/` пакета и ключ раскладки. */\nexport type TKind =\n | 'laws'\n | 'rules'\n | 'patterns'\n | 'skills'\n | 'hooks'\n | 'defaults'\n | 'checks'\n | 'agents'\n | 'commands'\n | 'workflows'\n | 'templates'\n /** Документы дерева, которые читает и человек, и агент: словарь проекта. */\n | 'docs'\n /**\n * Образцы, которые копируют в рабочий файл и заполняют: папка задачи, спек домена.\n *\n * От шаблонов отличаются читателем и местом. Шаблон ложится в каталог слоя правил, и с него\n * пишут ресурс этого слоя; образец ложится туда, где идёт работа, и копирует его тот, кто\n * работу ведёт. Одним родом их не свести: дерево вправе отказаться от одних, оставив\n * другие, и назвать им разные каталоги.\n */\n | 'samples';\n\nexport const KINDS: readonly TKind[] = [\n 'laws',\n 'rules',\n 'patterns',\n 'skills',\n 'hooks',\n 'defaults',\n 'checks',\n 'agents',\n 'commands',\n 'workflows',\n 'templates',\n 'docs',\n 'samples',\n];\n\n/**\n * Роды, которые ложатся не файлом в каталог, а каталогом по имени ресурса: скил читается как\n * `<имя>/SKILL.md`, и рядом с ним лежит то, что пишет проект.\n *\n * `skills` — скил, у которого нет закона над собой: он не про то, что должно быть верно в\n * продукте, а про то, как здесь делается работа. Правилом его называть нельзя — тогда придётся\n * выдумать ему закон, — а паттерном тем более: паттерн стоит при правиле.\n */\nexport const SKILL_KINDS: readonly TKind[] = ['rules', 'patterns', 'skills'];\n\n/** Имя файла скила: так его ищет агент, и другого имени у него быть не может. */\nexport const SKILL_FILE: string = 'SKILL.md';\n\n/**\n * Файл-компаньон правила: чем названо в этом дереве то, о чём правило говорит приёмом.\n *\n * Пакет знает приём, но не знает ни путей, ни имён — их пишет проект. Черновик кладётся один\n * раз и дальше не сверяется: это единственный файл раскладки, который принадлежит проекту.\n */\nexport const COMPANION_FILE: string = 'implementation.md';\n\nexport interface IConfig {\n /** Значения дырок `{{имя}}`. */\n readonly vars: Readonly<Record<string, string>>;\n /** Куда класть каждый род ресурса, путями от корня проекта. */\n readonly layout: Readonly<Record<TKind, string>>;\n /**\n * Ресурсы, которые проект выбрал, идентификаторами: `laws/access.md`. Пусто — берётся всё.\n * Ограничивает только те роды, которые сам называет; как именно — `isChosen`.\n */\n readonly only: readonly string[];\n /** Ресурсы, от которых проект отказался, идентификаторами: `laws/application/money.md`. */\n readonly skip: readonly string[];\n /**\n * Выбор проекта по осям различия: `{ \"host\": \"gitlab\" }`. Оси объявлены пакетом в\n * `assets/variants.json`, а ресурс с вариантом в имени берётся, только если проект выбрал\n * его значение; как именно — `isChosen`.\n */\n readonly variants: Readonly<Record<string, string>>;\n /**\n * Что у дерева есть: `[\"packages\", \"app\"]`. Свойства объявлены пакетом в `assets/traits.json`,\n * а ресурс, требующий свойства именем файла, дереву без него не кладётся вовсе — ни сам, ни\n * черновик его компаньона. Пусто — дерево о себе ничего не сказало, и помеченного не получает.\n */\n readonly has: readonly string[];\n /**\n * Пишут ли гарды наблюдения о слое правил. Умолчание — да; выключает дерево, и целиком, а\n * не частями. Ключ читают двое: сводка — отсюда, гард — тем же именем прямо из файла, потому\n * что разборщика конфига у него нет.\n */\n readonly observe: boolean;\n /**\n * Адрес приёма, куда уезжает груз: `https://…`. Пусто — отправлять некуда, и отправка\n * отказывает вместо молчания. Зашитый в код адрес назвал бы чужое дерево в текстах пакета.\n */\n readonly intake: string;\n /**\n * Файл с токеном дерева — путь от корня дерева или от домашнего каталога (`~/…`). Сам токен\n * в конфиге не лежит: конфиг коммитится, а токен обязан остаться вне истории.\n */\n readonly token: string;\n /**\n * Признак дерева, когда удалённого репозитория нет. При нём — считается из адреса\n * репозитория: две рабочие копии одного репозитория обязаны дать один признак.\n */\n readonly tree: string;\n /** Где дерево держит разборы происшествий. Путь от корня дерева. */\n readonly postmortems: string;\n}\n\nexport const CONFIG_PATH: string = '.claude/rt-kit.json';\n/** Каталог, в котором дерево держит своё при пакете: надстройки, умолчания, свою карту и профиль. */\nexport const RT_KIT_DIR: string = '.claude/rt-kit';\nexport const OVERRIDES_DIR: string = '.claude/rt-kit/overrides';\n\n/**\n * Где разборы происшествий лежат, когда конфиг не сказал иначе. Роды ресурсов сюда не годятся:\n * разборы пишет дерево про себя, пакет их не раскладывает и не сверяет.\n */\nexport const DEFAULT_POSTMORTEMS_DIR: string = 'docs/postmortems';\n\n/**\n * Карта «что правится — какое правило» и профиль дерева: команды, стенды, пары «правка —\n * документ». Оба лежат в двух видах, и это не дублирование, а разделение ответственности.\n *\n * Умолчание везёт пакет: проекты этой мастерской устроены одинаково — Nx, те же расширения, те\n * же имена каталогов, — и переписывать одну и ту же карту в каждом дереве заново значило бы\n * заводить пятнадцать её редакций, расходящихся молча. Оно разложено, под шапкой, и правится\n * не на месте.\n *\n * Надстройку пишет проект, и она необязательна: витрина, свой род файлов, другой запускатель\n * тестов есть не у всех. Объявленная в ней функция замещает умолчание целиком и вправе позвать\n * его обратно суффиксом `_default` — так дерево дописывает своё, не теряя общего.\n */\nexport const DEFAULTS_DIR: string = '.claude/rt-kit/defaults';\nexport const GATE_MAP_FILE: string = 'gate-map.sh';\nexport const PROFILE_FILE: string = 'project.sh';\n\n/**\n * Умолчания раскладки. Это не единственно возможные пути, но менять их без нужды не стоит:\n * агент ищет законы в `docs/constitution`, потому что так написано в правилах, которые пакет\n * же и везёт.\n */\nexport const DEFAULT_LAYOUT: Readonly<Record<TKind, string>> = {\n laws: 'docs/constitution',\n rules: '.claude/skills',\n patterns: '.claude/skills',\n skills: '.claude/skills',\n hooks: '.claude/hooks',\n defaults: DEFAULTS_DIR,\n checks: 'tools',\n agents: '.claude/agents',\n commands: '.claude/commands',\n workflows: '.claude/workflows',\n templates: '.claude/rt-kit/templates',\n docs: 'docs',\n // Внутри рода путь повторяет раскладку дерева — папки задач и спеки, — поэтому одного\n // умолчания хватает всем образцам сразу.\n samples: 'docs',\n};\n\nexport class ConfigError extends Error {}\n\nconst isRecord: (value: unknown) => value is Record<string, unknown> = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nfunction stringMap(value: unknown, where: string): Record<string, string> {\n if (value === undefined) {\n return {};\n }\n if (!isRecord(value)) {\n throw new ConfigError(`${where}: ожидается объект «имя — значение»`);\n }\n const result: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry !== 'string') {\n throw new ConfigError(`${where}.${key}: ожидается строка`);\n }\n result[key] = entry;\n }\n\n return result;\n}\n\n/** Строковый ключ конфига. Ключа нет — пустая строка: значение необязательное у всех четырёх. */\nfunction textOf(value: unknown, where: string): string {\n if (value === undefined || value === null) {\n return '';\n }\n if (typeof value !== 'string') {\n throw new ConfigError(`${where}: ожидается строка`);\n }\n\n return value.trim();\n}\n\nfunction idList(value: unknown, where: string): readonly string[] {\n const list: unknown = value ?? [];\n if (!Array.isArray(list) || list.some((entry: unknown): boolean => typeof entry !== 'string')) {\n throw new ConfigError(`${where}: ожидается список идентификаторов ресурсов`);\n }\n\n return list as string[];\n}\n\nexport function parseConfig(text: string): IConfig {\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (error: unknown) {\n throw new ConfigError(`${CONFIG_PATH} — не разбирается как JSON: ${(error as Error).message}`);\n }\n if (!isRecord(raw)) {\n throw new ConfigError(`${CONFIG_PATH}: ожидается объект`);\n }\n\n const layout: Record<string, string> = { ...DEFAULT_LAYOUT, ...stringMap(raw['layout'], 'layout') };\n for (const kind of Object.keys(layout)) {\n if (!KINDS.includes(kind as TKind)) {\n throw new ConfigError(`layout.${kind}: такого рода ресурсов нет — есть ${KINDS.join(', ')}`);\n }\n }\n\n const only: readonly string[] = idList(raw['only'], 'only');\n const skip: readonly string[] = idList(raw['skip'], 'skip');\n\n // Пути раскладки — тоже значения проекта, и пакет их уже знает. Требовать их вторым списком\n // значило бы держать одно и то же в двух местах: расходятся такие пары молча.\n const derived: Record<string, string> = {};\n for (const kind of KINDS) {\n derived[`${kind.replace(/s$/, '')}sDir`] = layout[kind];\n }\n\n return {\n vars: { ...derived, ...stringMap(raw['vars'], 'vars') },\n layout: layout as Record<TKind, string>,\n only,\n skip,\n variants: stringMap(raw['variants'], 'variants'),\n has: idList(raw['has'], 'has'),\n // Выключателем считается только явное «нет»: ключа нет — запись идёт, как и у гарда.\n observe: raw['observe'] !== false,\n intake: textOf(raw['intake'], 'intake'),\n token: textOf(raw['token'], 'token'),\n tree: textOf(raw['tree'], 'tree'),\n postmortems: textOf(raw['postmortems'], 'postmortems') || DEFAULT_POSTMORTEMS_DIR,\n };\n}\n\n/** Конфиг проекта; его отсутствие — не отказ, а повод сказать про `agent-kit init`. */\nexport function readConfig(root: string): IConfig | null {\n try {\n return parseConfig(readFileSync(join(root, CONFIG_PATH), 'utf8'));\n } catch (error: unknown) {\n if (error instanceof ConfigError) {\n throw error;\n }\n\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Конфиг проекта — `.claude/rt-kit.json`.\n *\n * Всё, что пакет не может знать сам: значения для дырок, куда класть каждый род ресурса и от\n * чего проект отказался. Конфиг лежит в дереве проекта и коммитится: раскладка обязана\n * повторяться на чужой машине без вопросов, иначе разложенное расходится между разработчиками.\n */\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Род ресурса. Он же имя каталога в `assets/` пакета и ключ раскладки. */\nexport type TKind =\n | 'laws'\n | 'rules'\n | 'patterns'\n | 'skills'\n | 'hooks'\n | 'defaults'\n | 'checks'\n | 'agents'\n | 'commands'\n | 'workflows'\n | 'templates'\n /** Документы дерева, которые читает и человек, и агент: словарь проекта. */\n | 'docs'\n /**\n * Образцы, которые копируют в рабочий файл и заполняют: папка задачи, спек домена.\n *\n * От шаблонов отличаются читателем и местом. Шаблон ложится в каталог слоя правил, и с него\n * пишут ресурс этого слоя; образец ложится туда, где идёт работа, и копирует его тот, кто\n * работу ведёт. Одним родом их не свести: дерево вправе отказаться от одних, оставив\n * другие, и назвать им разные каталоги.\n */\n | 'samples';\n\nexport const KINDS: readonly TKind[] = [\n 'laws',\n 'rules',\n 'patterns',\n 'skills',\n 'hooks',\n 'defaults',\n 'checks',\n 'agents',\n 'commands',\n 'workflows',\n 'templates',\n 'docs',\n 'samples',\n];\n\n/**\n * Роды, которые ложатся не файлом в каталог, а каталогом по имени ресурса: скил читается как\n * `<имя>/SKILL.md`, и рядом с ним лежит то, что пишет проект.\n *\n * `skills` — скил, у которого нет закона над собой: он не про то, что должно быть верно в\n * продукте, а про то, как здесь делается работа. Правилом его называть нельзя — тогда придётся\n * выдумать ему закон, — а паттерном тем более: паттерн стоит при правиле.\n */\nexport const SKILL_KINDS: readonly TKind[] = ['rules', 'patterns', 'skills'];\n\n/** Имя файла скила: так его ищет агент, и другого имени у него быть не может. */\nexport const SKILL_FILE: string = 'SKILL.md';\n\n/**\n * Файл-компаньон правила: чем названо в этом дереве то, о чём правило говорит приёмом.\n *\n * Пакет знает приём, но не знает ни путей, ни имён — их пишет проект. Черновик кладётся один\n * раз и дальше не сверяется: это единственный файл раскладки, который принадлежит проекту.\n */\nexport const COMPANION_FILE: string = 'implementation.md';\n\nexport interface IConfig {\n /** Значения дырок `{{имя}}`. */\n readonly vars: Readonly<Record<string, string>>;\n /** Куда класть каждый род ресурса, путями от корня проекта. */\n readonly layout: Readonly<Record<TKind, string>>;\n /**\n * Ресурсы, которые проект выбрал, идентификаторами: `laws/access.md`. Пусто — берётся всё.\n * Ограничивает только те роды, которые сам называет; как именно — `isChosen`.\n */\n readonly only: readonly string[];\n /** Ресурсы, от которых проект отказался, идентификаторами: `laws/application/money.md`. */\n readonly skip: readonly string[];\n /**\n * Выбор проекта по осям различия: `{ \"host\": \"gitlab\" }`. Оси объявлены пакетом в\n * `assets/variants.json`, а ресурс с вариантом в имени берётся, только если проект выбрал\n * его значение; как именно — `isChosen`.\n */\n readonly variants: Readonly<Record<string, string>>;\n /**\n * Что у дерева есть: `[\"packages\", \"app\"]`. Свойства объявлены пакетом в `assets/traits.json`,\n * а ресурс, требующий свойства именем файла, дереву без него не кладётся вовсе — ни сам, ни\n * черновик его компаньона. Пусто — дерево о себе ничего не сказало, и помеченного не получает.\n */\n readonly has: readonly string[];\n /**\n * Пишут ли гарды наблюдения о слое правил. Умолчание — да; выключает дерево, и целиком, а\n * не частями. Ключ читают двое: сводка — отсюда, гард — тем же именем прямо из файла, потому\n * что разборщика конфига у него нет.\n */\n readonly observe: boolean;\n /**\n * Адрес приёма, куда уезжает груз: `https://…`. Пусто — отправлять некуда, и отправка\n * отказывает вместо молчания. Зашитый в код адрес назвал бы чужое дерево в текстах пакета.\n */\n readonly intake: string;\n /**\n * Файл с токеном дерева — путь от корня дерева или от домашнего каталога (`~/…`). Сам токен\n * в конфиге не лежит: конфиг коммитится, а токен обязан остаться вне истории.\n */\n readonly token: string;\n /**\n * Признак дерева, когда удалённого репозитория нет. При нём — считается из адреса\n * репозитория: две рабочие копии одного репозитория обязаны дать один признак.\n */\n readonly tree: string;\n /** Где дерево держит разборы происшествий. Путь от корня дерева. */\n readonly postmortems: string;\n}\n\nexport const CONFIG_PATH: string = '.claude/rt-kit.json';\n/** Каталог, в котором дерево держит своё при пакете: надстройки, умолчания, свою карту и профиль. */\nexport const RT_KIT_DIR: string = '.claude/rt-kit';\nexport const OVERRIDES_DIR: string = '.claude/rt-kit/overrides';\n\n/**\n * Где разборы происшествий лежат, когда конфиг не сказал иначе. Роды ресурсов сюда не годятся:\n * разборы пишет дерево про себя, пакет их не раскладывает и не сверяет.\n */\nexport const DEFAULT_POSTMORTEMS_DIR: string = 'docs/postmortems';\n\n/**\n * Карта «что правится — какое правило» и профиль дерева: команды, стенды, пары «правка —\n * документ». Оба лежат в двух видах, и это не дублирование, а разделение ответственности.\n *\n * Умолчание везёт пакет: проекты этой мастерской устроены одинаково — Nx, те же расширения, те\n * же имена каталогов, — и переписывать одну и ту же карту в каждом дереве заново значило бы\n * заводить пятнадцать её редакций, расходящихся молча. Оно разложено, под шапкой, и правится\n * не на месте.\n *\n * Надстройку пишет проект, и она необязательна: витрина, свой род файлов, другой запускатель\n * тестов есть не у всех. Объявленная в ней функция замещает умолчание целиком и вправе позвать\n * его обратно суффиксом `_default` — так дерево дописывает своё, не теряя общего.\n */\n/** Каталог правил дерева: законы туда не идут, а правила, паттерны и скилы лежат вместе. */\nexport const SKILLS_DIR: string = '.claude/skills';\n\nexport const DEFAULTS_DIR: string = '.claude/rt-kit/defaults';\nexport const GATE_MAP_FILE: string = 'gate-map.sh';\nexport const PROFILE_FILE: string = 'project.sh';\n\n/**\n * Умолчания раскладки. Это не единственно возможные пути, но менять их без нужды не стоит:\n * агент ищет законы в `docs/constitution`, потому что так написано в правилах, которые пакет\n * же и везёт.\n */\nexport const DEFAULT_LAYOUT: Readonly<Record<TKind, string>> = {\n laws: 'docs/constitution',\n rules: SKILLS_DIR,\n patterns: SKILLS_DIR,\n skills: SKILLS_DIR,\n hooks: '.claude/hooks',\n defaults: DEFAULTS_DIR,\n checks: 'tools',\n agents: '.claude/agents',\n commands: '.claude/commands',\n workflows: '.claude/workflows',\n templates: '.claude/rt-kit/templates',\n docs: 'docs',\n // Внутри рода путь повторяет раскладку дерева — папки задач и спеки, — поэтому одного\n // умолчания хватает всем образцам сразу.\n samples: 'docs',\n};\n\nexport class ConfigError extends Error {}\n\nconst isRecord: (value: unknown) => value is Record<string, unknown> = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nfunction stringMap(value: unknown, where: string): Record<string, string> {\n if (value === undefined) {\n return {};\n }\n if (!isRecord(value)) {\n throw new ConfigError(`${where}: ожидается объект «имя — значение»`);\n }\n const result: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry !== 'string') {\n throw new ConfigError(`${where}.${key}: ожидается строка`);\n }\n result[key] = entry;\n }\n\n return result;\n}\n\n/** Строковый ключ конфига. Ключа нет — пустая строка: значение необязательное у всех четырёх. */\nfunction textOf(value: unknown, where: string): string {\n if (value === undefined || value === null) {\n return '';\n }\n if (typeof value !== 'string') {\n throw new ConfigError(`${where}: ожидается строка`);\n }\n\n return value.trim();\n}\n\nfunction idList(value: unknown, where: string): readonly string[] {\n const list: unknown = value ?? [];\n if (!Array.isArray(list) || list.some((entry: unknown): boolean => typeof entry !== 'string')) {\n throw new ConfigError(`${where}: ожидается список идентификаторов ресурсов`);\n }\n\n return list as string[];\n}\n\nexport function parseConfig(text: string): IConfig {\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (error: unknown) {\n throw new ConfigError(`${CONFIG_PATH} — не разбирается как JSON: ${(error as Error).message}`);\n }\n if (!isRecord(raw)) {\n throw new ConfigError(`${CONFIG_PATH}: ожидается объект`);\n }\n\n const layout: Record<string, string> = { ...DEFAULT_LAYOUT, ...stringMap(raw['layout'], 'layout') };\n for (const kind of Object.keys(layout)) {\n if (!KINDS.includes(kind as TKind)) {\n throw new ConfigError(`layout.${kind}: такого рода ресурсов нет — есть ${KINDS.join(', ')}`);\n }\n }\n\n const only: readonly string[] = idList(raw['only'], 'only');\n const skip: readonly string[] = idList(raw['skip'], 'skip');\n\n // Пути раскладки — тоже значения проекта, и пакет их уже знает. Требовать их вторым списком\n // значило бы держать одно и то же в двух местах: расходятся такие пары молча.\n const derived: Record<string, string> = {};\n for (const kind of KINDS) {\n derived[`${kind.replace(/s$/, '')}sDir`] = layout[kind];\n }\n\n return {\n only,\n skip,\n vars: { ...derived, ...stringMap(raw['vars'], 'vars') },\n layout: layout as Record<TKind, string>,\n variants: stringMap(raw['variants'], 'variants'),\n has: idList(raw['has'], 'has'),\n // Выключателем считается только явное «нет»: ключа нет — запись идёт, как и у гарда.\n observe: raw['observe'] !== false,\n intake: textOf(raw['intake'], 'intake'),\n token: textOf(raw['token'], 'token'),\n tree: textOf(raw['tree'], 'tree'),\n postmortems: textOf(raw['postmortems'], 'postmortems') || DEFAULT_POSTMORTEMS_DIR,\n };\n}\n\n/** Конфиг проекта; его отсутствие — не отказ, а повод сказать про `agent-kit init`. */\nexport function readConfig(root: string): IConfig | null {\n try {\n return parseConfig(readFileSync(join(root, CONFIG_PATH), 'utf8'));\n } catch (error: unknown) {\n if (error instanceof ConfigError) {\n throw error;\n }\n\n return null;\n }\n}\n"]}
|
package/lib/enroll.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IEnrollGranted } from './cargo.js';
|
|
2
|
+
/** Тело обращения: признак дерева и код приглашения. Имени в нём нет — его знает приглашение. */
|
|
3
|
+
export interface IEnrollBody {
|
|
4
|
+
readonly schema: string;
|
|
5
|
+
readonly tree: string;
|
|
6
|
+
readonly code: string;
|
|
7
|
+
}
|
|
8
|
+
/** Чем кончилось обращение. Отказ — такой же ответ, как выданный токен: его печатает вызывающий. */
|
|
9
|
+
export interface IEnrollAnswer {
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
/** Код ответа. Ноль — приём не ответил вовсе: не дозвонились, оборвалось, вышло время. */
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly said: string;
|
|
14
|
+
/** Токен и имя дерева. Пусто у отказа: приём называет их только у годного обращения. */
|
|
15
|
+
readonly granted: IEnrollGranted | null;
|
|
16
|
+
}
|
|
17
|
+
/** Чем обращение уходит в приём. Двойник в спеке — того же вида. */
|
|
18
|
+
export type TEnrollCall = (intake: string, body: IEnrollBody) => Promise<IEnrollAnswer>;
|
|
19
|
+
/** Что нужно заведению: где дерево, куда идти, чем платить и куда класть токен. */
|
|
20
|
+
export interface IEnrollOptions {
|
|
21
|
+
readonly root: string;
|
|
22
|
+
readonly intake: string;
|
|
23
|
+
readonly code: string;
|
|
24
|
+
/**
|
|
25
|
+
* Токен, выданный человеку прямо в админке приёма.
|
|
26
|
+
*
|
|
27
|
+
* Второй путь завести дерево, и сети он не требует вовсе: токен уже на руках, обменивать не
|
|
28
|
+
* на что. С кодом приглашения не сочетается — это два пути к одному и тому же.
|
|
29
|
+
*/
|
|
30
|
+
readonly issued: string;
|
|
31
|
+
/** Признак дерева, посчитанный от адреса репозитория. */
|
|
32
|
+
readonly tree: string;
|
|
33
|
+
/** Файл токена — путь от корня дерева или от домашнего каталога. */
|
|
34
|
+
readonly token: string;
|
|
35
|
+
/** Перезаписать лежащий токен намеренно. Без него дерево с токеном обращения не шлёт. */
|
|
36
|
+
readonly force: boolean;
|
|
37
|
+
readonly call: TEnrollCall;
|
|
38
|
+
}
|
|
39
|
+
/** Что команда напечатала и чем кончилась. Строки печатает вызывающий. */
|
|
40
|
+
export interface IEnrollOutcome {
|
|
41
|
+
readonly code: number;
|
|
42
|
+
readonly lines: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Годен ли адрес приёма для обращения без TLS.
|
|
46
|
+
*
|
|
47
|
+
* Токен уходит единственным ответом на это обращение, и по открытому пути его читает всякий, кто
|
|
48
|
+
* стоит между сторонами. Исключение одно — локальная машина: там приём поднимают для проверки, и
|
|
49
|
+
* сети между сторонами нет вовсе.
|
|
50
|
+
*/
|
|
51
|
+
export declare function intakeAllowed(intake: string): boolean;
|
|
52
|
+
/** Куда ложится токен: путь от домашнего каталога или от корня дерева. */
|
|
53
|
+
export declare function tokenPath(root: string, spoken: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Заведение дерева.
|
|
56
|
+
*
|
|
57
|
+
* Порядок проверок такой, что до сети доходит только то, что имеет шанс: незаполненная настройка,
|
|
58
|
+
* открытый адрес и уже лежащий токен отбиваются здесь, у себя. Общие обоим путям проверки стоят
|
|
59
|
+
* до развилки: поставленные после, они защищали бы только один из них.
|
|
60
|
+
*/
|
|
61
|
+
export declare function enroll(options: IEnrollOptions): Promise<IEnrollOutcome>;
|
|
62
|
+
/** Обращение в приём по-настоящему. Код уезжает телом и в заголовки не попадает. */
|
|
63
|
+
export declare const httpEnroll: TEnrollCall;
|
|
64
|
+
//# sourceMappingURL=enroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enroll.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/enroll.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAM5C,iGAAiG;AACjG,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED,oGAAoG;AACpG,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;CAC3C;AAED,oEAAoE;AACpE,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAExF,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yFAAyF;IACzF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC9B;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AA+BD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQrD;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AAmFD;;;;;;GAMG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CA2B7E;AAED,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,WAiBxB,CAAC"}
|
package/lib/enroll.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Заведение дерева: токен ложится на диск правами «читает и пишет только владелец файла».
|
|
3
|
+
*
|
|
4
|
+
* Путей два, и человек выбирает доводом.
|
|
5
|
+
*
|
|
6
|
+
* Обмен по коду — дерево просит токен у приёма, неся одноразовый код. Это вторая и последняя
|
|
7
|
+
* команда пакета, которая ходит в сеть, и единственная, которая ходит туда без токена: токена у
|
|
8
|
+
* дерева ещё нет, за ним и обращаются.
|
|
9
|
+
*
|
|
10
|
+
* Выданный токен — тот, что человек получил прямо в админке приёма. Обменивать не на что, и в
|
|
11
|
+
* сеть команда не идёт вовсе: она кладёт полученное на диск теми же правами. Отсюда и разный
|
|
12
|
+
* набор проверок — адрес приёма и запрет открытого пути этому пути не нужны.
|
|
13
|
+
*
|
|
14
|
+
* Ходит она только по слову человека. Гарды в сеть не ходят вовсе, и заведение — не исключение:
|
|
15
|
+
* дерево заводят один раз, руками.
|
|
16
|
+
*
|
|
17
|
+
* Сеть здесь отделена от решения так же, как у отправки груза: чем сходить, приходит доводом, и
|
|
18
|
+
* спека подставляет двойник, никуда не стучась.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { intakeUrl, SHIP_TIMEOUT_MS } from './ship.js';
|
|
24
|
+
/** Права файла с токеном: читает и пишет только владелец файла. */
|
|
25
|
+
const TOKEN_MODE = 0o600;
|
|
26
|
+
/** Отказ команды: причина строками и ненулевой код выхода. */
|
|
27
|
+
function refusal(...lines) {
|
|
28
|
+
return { code: 1, lines };
|
|
29
|
+
}
|
|
30
|
+
/** Что сказал приём: его собственное сообщение, а если тело не разбирается — тело как приехало. */
|
|
31
|
+
function saidOf(text) {
|
|
32
|
+
try {
|
|
33
|
+
const said = JSON.parse(text);
|
|
34
|
+
return typeof said['message'] === 'string' ? said['message'] : text.trim();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return text.trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Токен и имя из ответа приёма. Ответ не тем — пусто: своё устройство приём не пересказывает. */
|
|
41
|
+
function grantedOf(text) {
|
|
42
|
+
try {
|
|
43
|
+
const said = JSON.parse(text);
|
|
44
|
+
return typeof said['token'] === 'string' && typeof said['tree'] === 'string' && typeof said['name'] === 'string'
|
|
45
|
+
? { tree: said['tree'], name: said['name'], token: said['token'] }
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Годен ли адрес приёма для обращения без TLS.
|
|
54
|
+
*
|
|
55
|
+
* Токен уходит единственным ответом на это обращение, и по открытому пути его читает всякий, кто
|
|
56
|
+
* стоит между сторонами. Исключение одно — локальная машина: там приём поднимают для проверки, и
|
|
57
|
+
* сети между сторонами нет вовсе.
|
|
58
|
+
*/
|
|
59
|
+
export function intakeAllowed(intake) {
|
|
60
|
+
const address = intake.trim().toLowerCase();
|
|
61
|
+
if (address.startsWith('https://')) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])([:/]|$)/.test(address);
|
|
65
|
+
}
|
|
66
|
+
/** Куда ложится токен: путь от домашнего каталога или от корня дерева. */
|
|
67
|
+
export function tokenPath(root, spoken) {
|
|
68
|
+
return spoken.startsWith('~/') ? join(homedir(), spoken.slice(2)) : join(root, spoken);
|
|
69
|
+
}
|
|
70
|
+
/** Запись токена на диск: у обоих путей она одна, и права файла у неё одни. */
|
|
71
|
+
function lay(path, token) {
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
73
|
+
writeFileSync(path, `${token}\n`, { encoding: 'utf8', mode: TOKEN_MODE });
|
|
74
|
+
}
|
|
75
|
+
/** Токен с рук: приём о нём не спрашивается — он же его и выдал, и обменивать не на что. */
|
|
76
|
+
function accept(options, path) {
|
|
77
|
+
lay(path, options.issued);
|
|
78
|
+
return {
|
|
79
|
+
code: 0,
|
|
80
|
+
lines: [
|
|
81
|
+
'дерево заведено токеном, выданным в админке приёма',
|
|
82
|
+
`токен лежит в ${path}, читать и писать его может только владелец файла`,
|
|
83
|
+
'проверить связь с приёмом: `agent-kit propose --dry-run`',
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Обращение и запись токена: сюда доходит только то, что прошло проверки у себя. */
|
|
88
|
+
async function grant(options, path) {
|
|
89
|
+
const answer = await options.call(options.intake, { schema: '1', tree: options.tree, code: options.code });
|
|
90
|
+
if (!answer.ok || !answer.granted) {
|
|
91
|
+
return refusal(answer.status === 0 ? `приём не ответил: ${answer.said}` : `приём отказал (${answer.status}): ${answer.said}`);
|
|
92
|
+
}
|
|
93
|
+
lay(path, answer.granted.token);
|
|
94
|
+
return {
|
|
95
|
+
code: 0,
|
|
96
|
+
lines: [
|
|
97
|
+
`дерево заведено: «${answer.granted.name}» (${answer.granted.tree})`,
|
|
98
|
+
`токен лежит в ${path}, читать и писать его может только владелец файла`,
|
|
99
|
+
'приглашение погашено: второй раз им не воспользоваться',
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Выбран ли путь: отказ, когда доводов два или ни одного, и пусто, когда выбран ровно один.
|
|
105
|
+
*
|
|
106
|
+
* Стоит отдельно от самого заведения: разбор довода и порядок проверок — два разных решения, и
|
|
107
|
+
* вместе они не читаются.
|
|
108
|
+
*/
|
|
109
|
+
function pathChosen(options) {
|
|
110
|
+
if (options.code && options.issued) {
|
|
111
|
+
return refusal('код приглашения и выданный токен — два разных пути завести дерево, и вместе они не идут', 'оставьте один довод: --code <код> либо --token <токен>');
|
|
112
|
+
}
|
|
113
|
+
if (!options.code && !options.issued) {
|
|
114
|
+
return refusal('заведение ждёт одно из двух:', ' --code <код> код приглашения, выданный владельцем приёма', ' --token <токен> токен, выданный человеку прямо в админке приёма');
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/** Годен ли приём для обмена по коду. Пути с рук эти проверки не касаются: он в сеть не идёт. */
|
|
119
|
+
function intakeReady(options) {
|
|
120
|
+
if (!options.intake) {
|
|
121
|
+
return refusal('в настройке дерева не назван адрес приёма: заполните ключ `intake`');
|
|
122
|
+
}
|
|
123
|
+
if (!intakeAllowed(options.intake)) {
|
|
124
|
+
return refusal(`адрес приёма ${options.intake} без TLS: токен уходит единственным ответом, и по открытому пути его прочитает всякий`, 'поправьте ключ `intake` на https://… — исключение сделано только для локальной машины');
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Заведение дерева.
|
|
130
|
+
*
|
|
131
|
+
* Порядок проверок такой, что до сети доходит только то, что имеет шанс: незаполненная настройка,
|
|
132
|
+
* открытый адрес и уже лежащий токен отбиваются здесь, у себя. Общие обоим путям проверки стоят
|
|
133
|
+
* до развилки: поставленные после, они защищали бы только один из них.
|
|
134
|
+
*/
|
|
135
|
+
export async function enroll(options) {
|
|
136
|
+
const chosen = pathChosen(options);
|
|
137
|
+
if (chosen) {
|
|
138
|
+
return chosen;
|
|
139
|
+
}
|
|
140
|
+
if (!options.token) {
|
|
141
|
+
return refusal('в настройке дерева не назван файл токена: заполните ключ `token`');
|
|
142
|
+
}
|
|
143
|
+
const laid = tokenPath(options.root, options.token);
|
|
144
|
+
if (existsSync(laid) && !options.force) {
|
|
145
|
+
return refusal(`токен уже лежит в ${laid}: заведённое дерево потеряло бы связь со своим прежним грузом`, 'если это намеренно — позовите ту же команду с доводом --force');
|
|
146
|
+
}
|
|
147
|
+
if (options.issued) {
|
|
148
|
+
return accept(options, laid);
|
|
149
|
+
}
|
|
150
|
+
const reachable = intakeReady(options);
|
|
151
|
+
return reachable ?? grant(options, laid);
|
|
152
|
+
}
|
|
153
|
+
/** Обращение в приём по-настоящему. Код уезжает телом и в заголовки не попадает. */
|
|
154
|
+
export const httpEnroll = async (intake, body) => {
|
|
155
|
+
let answer;
|
|
156
|
+
try {
|
|
157
|
+
answer = await fetch(intakeUrl(intake, 'enroll'), {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'content-type': 'application/json' },
|
|
160
|
+
body: JSON.stringify(body),
|
|
161
|
+
signal: AbortSignal.timeout(SHIP_TIMEOUT_MS),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return { ok: false, status: 0, said: error.message, granted: null };
|
|
166
|
+
}
|
|
167
|
+
const text = await answer.text();
|
|
168
|
+
return { ok: answer.ok, status: answer.status, said: saidOf(text), granted: answer.ok ? grantedOf(text) : null };
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=enroll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enroll.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/enroll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEvD,mEAAmE;AACnE,MAAM,UAAU,GAAW,KAAK,CAAC;AAiDjC,8DAA8D;AAC9D,SAAS,OAAO,CAAC,GAAG,KAAwB;IACxC,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,mGAAmG;AACnG,SAAS,MAAM,CAAC,IAAY;IACxB,IAAI,CAAC;QACD,MAAM,IAAI,GAA4B,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QAElF,OAAO,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;AACL,CAAC;AAED,kGAAkG;AAClG,SAAS,SAAS,CAAC,IAAY;IAC3B,IAAI,CAAC;QACD,MAAM,IAAI,GAA4B,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QAElF,OAAO,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,QAAQ;YAC5G,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE;YAClE,CAAC,CAAC,IAAI,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IACxC,MAAM,OAAO,GAAW,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAEpD,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,OAAO,oDAAoD,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC9E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,MAAc;IAClD,OAAO,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3F,CAAC;AAED,+EAA+E;AAC/E,SAAS,GAAG,CAAC,IAAY,EAAE,KAAa;IACpC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,4FAA4F;AAC5F,SAAS,MAAM,CAAC,OAAuB,EAAE,IAAY;IACjD,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAE1B,OAAO;QACH,IAAI,EAAE,CAAC;QACP,KAAK,EAAE;YACH,oDAAoD;YACpD,iBAAiB,IAAI,mDAAmD;YACxE,0DAA0D;SAC7D;KACJ,CAAC;AACN,CAAC;AAED,qFAAqF;AACrF,KAAK,UAAU,KAAK,CAAC,OAAuB,EAAE,IAAY;IACtD,MAAM,MAAM,GAAkB,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAE1H,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,qBAAqB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,kBAAkB,MAAM,CAAC,MAAM,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAClI,CAAC;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAEhC,OAAO;QACH,IAAI,EAAE,CAAC;QACP,KAAK,EAAE;YACH,qBAAqB,MAAM,CAAC,OAAO,CAAC,IAAI,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,GAAG;YACpE,iBAAiB,IAAI,mDAAmD;YACxE,wDAAwD;SAC3D;KACJ,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,OAAuB;IACvC,IAAI,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACjC,OAAO,OAAO,CACV,yFAAyF,EACzF,wDAAwD,CAC3D,CAAC;IACN,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACnC,OAAO,OAAO,CACV,8BAA8B,EAC9B,gEAAgE,EAChE,oEAAoE,CACvE,CAAC;IACN,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,iGAAiG;AACjG,SAAS,WAAW,CAAC,OAAuB;IACxC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,OAAO,CAAC,oEAAoE,CAAC,CAAC;IACzF,CAAC;IAED,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACjC,OAAO,OAAO,CACV,gBAAgB,OAAO,CAAC,MAAM,uFAAuF,EACrH,uFAAuF,CAC1F,CAAC;IACN,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAAuB;IAChD,MAAM,MAAM,GAA0B,UAAU,CAAC,OAAO,CAAC,CAAC;IAE1D,IAAI,MAAM,EAAE,CAAC;QACT,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,OAAO,CAAC,kEAAkE,CAAC,CAAC;IACvF,CAAC;IAED,MAAM,IAAI,GAAW,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAE5D,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrC,OAAO,OAAO,CACV,qBAAqB,IAAI,+DAA+D,EACxF,+DAA+D,CAClE,CAAC;IACN,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,MAAM,SAAS,GAA0B,WAAW,CAAC,OAAO,CAAC,CAAC;IAE9D,OAAO,SAAS,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,MAAM,UAAU,GAAgB,KAAK,EAAE,MAAc,EAAE,IAAiB,EAA0B,EAAE;IACvG,IAAI,MAAgB,CAAC;IAErB,IAAI,CAAC;QACD,MAAM,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;YAC9C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAC1B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,eAAe,CAAC;SAC/C,CAAC,CAAC;IACP,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAG,KAAe,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACnF,CAAC;IAED,MAAM,IAAI,GAAW,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IAEzC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACrH,CAAC,CAAC","sourcesContent":["/**\n * Заведение дерева: токен ложится на диск правами «читает и пишет только владелец файла».\n *\n * Путей два, и человек выбирает доводом.\n *\n * Обмен по коду — дерево просит токен у приёма, неся одноразовый код. Это вторая и последняя\n * команда пакета, которая ходит в сеть, и единственная, которая ходит туда без токена: токена у\n * дерева ещё нет, за ним и обращаются.\n *\n * Выданный токен — тот, что человек получил прямо в админке приёма. Обменивать не на что, и в\n * сеть команда не идёт вовсе: она кладёт полученное на диск теми же правами. Отсюда и разный\n * набор проверок — адрес приёма и запрет открытого пути этому пути не нужны.\n *\n * Ходит она только по слову человека. Гарды в сеть не ходят вовсе, и заведение — не исключение:\n * дерево заводят один раз, руками.\n *\n * Сеть здесь отделена от решения так же, как у отправки груза: чем сходить, приходит доводом, и\n * спека подставляет двойник, никуда не стучась.\n */\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\nimport { IEnrollGranted } from './cargo.js';\nimport { intakeUrl, SHIP_TIMEOUT_MS } from './ship.js';\n\n/** Права файла с токеном: читает и пишет только владелец файла. */\nconst TOKEN_MODE: number = 0o600;\n\n/** Тело обращения: признак дерева и код приглашения. Имени в нём нет — его знает приглашение. */\nexport interface IEnrollBody {\n readonly schema: string;\n readonly tree: string;\n readonly code: string;\n}\n\n/** Чем кончилось обращение. Отказ — такой же ответ, как выданный токен: его печатает вызывающий. */\nexport interface IEnrollAnswer {\n readonly ok: boolean;\n /** Код ответа. Ноль — приём не ответил вовсе: не дозвонились, оборвалось, вышло время. */\n readonly status: number;\n readonly said: string;\n /** Токен и имя дерева. Пусто у отказа: приём называет их только у годного обращения. */\n readonly granted: IEnrollGranted | null;\n}\n\n/** Чем обращение уходит в приём. Двойник в спеке — того же вида. */\nexport type TEnrollCall = (intake: string, body: IEnrollBody) => Promise<IEnrollAnswer>;\n\n/** Что нужно заведению: где дерево, куда идти, чем платить и куда класть токен. */\nexport interface IEnrollOptions {\n readonly root: string;\n readonly intake: string;\n readonly code: string;\n /**\n * Токен, выданный человеку прямо в админке приёма.\n *\n * Второй путь завести дерево, и сети он не требует вовсе: токен уже на руках, обменивать не\n * на что. С кодом приглашения не сочетается — это два пути к одному и тому же.\n */\n readonly issued: string;\n /** Признак дерева, посчитанный от адреса репозитория. */\n readonly tree: string;\n /** Файл токена — путь от корня дерева или от домашнего каталога. */\n readonly token: string;\n /** Перезаписать лежащий токен намеренно. Без него дерево с токеном обращения не шлёт. */\n readonly force: boolean;\n readonly call: TEnrollCall;\n}\n\n/** Что команда напечатала и чем кончилась. Строки печатает вызывающий. */\nexport interface IEnrollOutcome {\n readonly code: number;\n readonly lines: readonly string[];\n}\n\n/** Отказ команды: причина строками и ненулевой код выхода. */\nfunction refusal(...lines: readonly string[]): IEnrollOutcome {\n return { code: 1, lines };\n}\n\n/** Что сказал приём: его собственное сообщение, а если тело не разбирается — тело как приехало. */\nfunction saidOf(text: string): string {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['message'] === 'string' ? said['message'] : text.trim();\n } catch {\n return text.trim();\n }\n}\n\n/** Токен и имя из ответа приёма. Ответ не тем — пусто: своё устройство приём не пересказывает. */\nfunction grantedOf(text: string): IEnrollGranted | null {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['token'] === 'string' && typeof said['tree'] === 'string' && typeof said['name'] === 'string'\n ? { tree: said['tree'], name: said['name'], token: said['token'] }\n : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Годен ли адрес приёма для обращения без TLS.\n *\n * Токен уходит единственным ответом на это обращение, и по открытому пути его читает всякий, кто\n * стоит между сторонами. Исключение одно — локальная машина: там приём поднимают для проверки, и\n * сети между сторонами нет вовсе.\n */\nexport function intakeAllowed(intake: string): boolean {\n const address: string = intake.trim().toLowerCase();\n\n if (address.startsWith('https://')) {\n return true;\n }\n\n return /^http:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])([:/]|$)/.test(address);\n}\n\n/** Куда ложится токен: путь от домашнего каталога или от корня дерева. */\nexport function tokenPath(root: string, spoken: string): string {\n return spoken.startsWith('~/') ? join(homedir(), spoken.slice(2)) : join(root, spoken);\n}\n\n/** Запись токена на диск: у обоих путей она одна, и права файла у неё одни. */\nfunction lay(path: string, token: string): void {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, `${token}\\n`, { encoding: 'utf8', mode: TOKEN_MODE });\n}\n\n/** Токен с рук: приём о нём не спрашивается — он же его и выдал, и обменивать не на что. */\nfunction accept(options: IEnrollOptions, path: string): IEnrollOutcome {\n lay(path, options.issued);\n\n return {\n code: 0,\n lines: [\n 'дерево заведено токеном, выданным в админке приёма',\n `токен лежит в ${path}, читать и писать его может только владелец файла`,\n 'проверить связь с приёмом: `agent-kit propose --dry-run`',\n ],\n };\n}\n\n/** Обращение и запись токена: сюда доходит только то, что прошло проверки у себя. */\nasync function grant(options: IEnrollOptions, path: string): Promise<IEnrollOutcome> {\n const answer: IEnrollAnswer = await options.call(options.intake, { schema: '1', tree: options.tree, code: options.code });\n\n if (!answer.ok || !answer.granted) {\n return refusal(answer.status === 0 ? `приём не ответил: ${answer.said}` : `приём отказал (${answer.status}): ${answer.said}`);\n }\n\n lay(path, answer.granted.token);\n\n return {\n code: 0,\n lines: [\n `дерево заведено: «${answer.granted.name}» (${answer.granted.tree})`,\n `токен лежит в ${path}, читать и писать его может только владелец файла`,\n 'приглашение погашено: второй раз им не воспользоваться',\n ],\n };\n}\n\n/**\n * Выбран ли путь: отказ, когда доводов два или ни одного, и пусто, когда выбран ровно один.\n *\n * Стоит отдельно от самого заведения: разбор довода и порядок проверок — два разных решения, и\n * вместе они не читаются.\n */\nfunction pathChosen(options: IEnrollOptions): IEnrollOutcome | null {\n if (options.code && options.issued) {\n return refusal(\n 'код приглашения и выданный токен — два разных пути завести дерево, и вместе они не идут',\n 'оставьте один довод: --code <код> либо --token <токен>'\n );\n }\n\n if (!options.code && !options.issued) {\n return refusal(\n 'заведение ждёт одно из двух:',\n ' --code <код> код приглашения, выданный владельцем приёма',\n ' --token <токен> токен, выданный человеку прямо в админке приёма'\n );\n }\n\n return null;\n}\n\n/** Годен ли приём для обмена по коду. Пути с рук эти проверки не касаются: он в сеть не идёт. */\nfunction intakeReady(options: IEnrollOptions): IEnrollOutcome | null {\n if (!options.intake) {\n return refusal('в настройке дерева не назван адрес приёма: заполните ключ `intake`');\n }\n\n if (!intakeAllowed(options.intake)) {\n return refusal(\n `адрес приёма ${options.intake} без TLS: токен уходит единственным ответом, и по открытому пути его прочитает всякий`,\n 'поправьте ключ `intake` на https://… — исключение сделано только для локальной машины'\n );\n }\n\n return null;\n}\n\n/**\n * Заведение дерева.\n *\n * Порядок проверок такой, что до сети доходит только то, что имеет шанс: незаполненная настройка,\n * открытый адрес и уже лежащий токен отбиваются здесь, у себя. Общие обоим путям проверки стоят\n * до развилки: поставленные после, они защищали бы только один из них.\n */\nexport async function enroll(options: IEnrollOptions): Promise<IEnrollOutcome> {\n const chosen: IEnrollOutcome | null = pathChosen(options);\n\n if (chosen) {\n return chosen;\n }\n\n if (!options.token) {\n return refusal('в настройке дерева не назван файл токена: заполните ключ `token`');\n }\n\n const laid: string = tokenPath(options.root, options.token);\n\n if (existsSync(laid) && !options.force) {\n return refusal(\n `токен уже лежит в ${laid}: заведённое дерево потеряло бы связь со своим прежним грузом`,\n 'если это намеренно — позовите ту же команду с доводом --force'\n );\n }\n\n if (options.issued) {\n return accept(options, laid);\n }\n\n const reachable: IEnrollOutcome | null = intakeReady(options);\n\n return reachable ?? grant(options, laid);\n}\n\n/** Обращение в приём по-настоящему. Код уезжает телом и в заголовки не попадает. */\nexport const httpEnroll: TEnrollCall = async (intake: string, body: IEnrollBody): Promise<IEnrollAnswer> => {\n let answer: Response;\n\n try {\n answer = await fetch(intakeUrl(intake, 'enroll'), {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(SHIP_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n return { ok: false, status: 0, said: (error as Error).message, granted: null };\n }\n\n const text: string = await answer.text();\n\n return { ok: answer.ok, status: answer.status, said: saidOf(text), granted: answer.ok ? grantedOf(text) : null };\n};\n"]}
|
package/lib/freshness.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/freshness.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,WAAW;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;
|
|
1
|
+
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/freshness.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,WAAW;IACxB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAuGD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAiBhF"}
|
package/lib/freshness.js
CHANGED
|
@@ -42,6 +42,9 @@ function newestIn(dir) {
|
|
|
42
42
|
else if (!found || stats.mtimeMs > found.at) {
|
|
43
43
|
found = { at: stats.mtimeMs, path };
|
|
44
44
|
}
|
|
45
|
+
else {
|
|
46
|
+
// Файл старше уже найденного: самым свежим он не станет
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
50
|
walk(dir);
|
|
@@ -67,26 +70,37 @@ function workspaceOf(start) {
|
|
|
67
70
|
dir = parent;
|
|
68
71
|
}
|
|
69
72
|
}
|
|
73
|
+
/** Имя пакета из его описания. Описания нет или оно не разбирается — имени нет. */
|
|
74
|
+
function namedPackage(dir) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name ?? null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
70
82
|
/** Каталог исходников пакета с этим именем внутри рабочего пространства. */
|
|
83
|
+
function sourceInGroup(dir, name) {
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = readdirSync(dir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const candidate = join(dir, entry);
|
|
93
|
+
if (namedPackage(candidate) === name) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
71
99
|
function sourceOf(workspace, name) {
|
|
72
100
|
for (const group of PROJECT_DIRS) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
const candidate = join(workspace, group, entry);
|
|
82
|
-
try {
|
|
83
|
-
if (JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')).name === name) {
|
|
84
|
-
return candidate;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
101
|
+
const found = sourceInGroup(join(workspace, group), name);
|
|
102
|
+
if (found !== null) {
|
|
103
|
+
return found;
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
return null;
|
package/lib/freshness.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"freshness.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAS,MAAM,SAAS,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,iGAAiG;AACjG,MAAM,eAAe,GAAsB,CAAC,SAAS,EAAE,qBAAqB,EAAE,YAAY,CAAC,CAAC;AAE5F,+DAA+D;AAC/D,MAAM,YAAY,GAAsB,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAcjF,+EAA+E;AAC/E,SAAS,QAAQ,CAAC,GAAW;IACzB,IAAI,KAAK,GAAmB,IAAI,CAAC;IAEjC,MAAM,IAAI,GAA8B,CAAC,OAAe,EAAQ,EAAE;QAC9D,IAAI,OAA0B,CAAC;QAC/B,IAAI,CAAC;YACD,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACL,OAAO;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YACzB,MAAM,IAAI,GAAW,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACzC,IAAI,KAAY,CAAC;YACjB,IAAI,CAAC;gBACD,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBACL,SAAS;YACb,CAAC;YACD,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACtB,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,CAAC;iBAAM,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,EAAE,CAAC;gBAC5C,KAAK,GAAG,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;YACxC,CAAC;QACL,CAAC;IACL,CAAC,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAa;IAC9B,IAAI,GAAG,GAAW,KAAK,CAAC;IACxB,SAAS,CAAC;QACN,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/E,OAAO,GAAG,CAAC;QACf,CAAC;QACD,MAAM,MAAM,GAAW,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,GAAG,GAAG,MAAM,CAAC;IACjB,CAAC;AACL,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"freshness.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/freshness.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Свежесть собранного пакета относительно его исходников.\n *\n * Строка запуска читает собранное, а правка ресурса идёт в исходники. Между ними стоит сборка,\n * и пока её не прогнали, `sync` отвечает «всё уже разложено» — притом что в дереве лежит\n * прежняя редакция ресурса. Неправда эта дороже отказа: её замечают, когда правленое правило не\n * действует, и ищут причину в раскладке, а не в том, что её не собрали.\n *\n * Исходники ищутся только там, где они есть: у того, кто пакет разрабатывает. У потребителя\n * рядом с установленным пакетом нет ни рабочего пространства, ни каталога проектов, и проверка\n * молчит — сверять не с чем и нечего.\n */\nimport { existsSync, readdirSync, readFileSync, statSync, Stats } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\n/** Признаки корня рабочего пространства: по ним ищется дерево, в котором пакет разрабатывают. */\nconst WORKSPACE_MARKS: readonly string[] = ['nx.json', 'pnpm-workspace.yaml', 'lerna.json'];\n\n/** Каталоги, в которых рабочее пространство держит проекты. */\nconst PROJECT_DIRS: readonly string[] = ['projects', 'packages', 'libs', 'apps'];\n\nexport interface IStaleBuild {\n /** Каталог ресурсов в исходниках. */\n readonly source: string;\n /** Файл ресурса, правленный позже сборки, — по нему видно, чего именно не хватает в дереве. */\n readonly newest: string;\n}\n\ninterface INewest {\n readonly at: number;\n readonly path: string;\n}\n\n/** Самое позднее время правки в поддереве и файл, которому оно принадлежит. */\nfunction newestIn(dir: string): INewest | null {\n let found: INewest | null = null;\n\n const walk: (current: string) => void = (current: string): void => {\n let entries: readonly string[];\n try {\n entries = readdirSync(current);\n } catch {\n return;\n }\n for (const name of entries) {\n const path: string = join(current, name);\n let stats: Stats;\n try {\n stats = statSync(path);\n } catch {\n continue;\n }\n if (stats.isDirectory()) {\n walk(path);\n } else if (!found || stats.mtimeMs > found.at) {\n found = { at: stats.mtimeMs, path };\n } else {\n // Файл старше уже найденного: самым свежим он не станет\n }\n }\n };\n walk(dir);\n\n return found;\n}\n\n/**\n * Корень рабочего пространства над этим каталогом.\n *\n * Подъём заканчивается на корне файловой системы, а не на отсчитанном числе шагов: глубина, на\n * которой лежит установленный пакет, у каждого дерева своя, и отсчитанный предел молчал бы ровно\n * там, где вложенность оказалась глубже придуманной.\n */\nfunction workspaceOf(start: string): string | null {\n let dir: string = start;\n for (;;) {\n if (WORKSPACE_MARKS.some((mark: string): boolean => existsSync(join(dir, mark)))) {\n return dir;\n }\n const parent: string = dirname(dir);\n if (parent === dir) {\n return null;\n }\n dir = parent;\n }\n}\n\n/** Имя пакета из его описания. Описания нет или оно не разбирается — имени нет. */\nfunction namedPackage(dir: string): string | null {\n try {\n return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name ?? null;\n } catch {\n return null;\n }\n}\n\n/** Каталог исходников пакета с этим именем внутри рабочего пространства. */\nfunction sourceInGroup(dir: string, name: string): string | null {\n let entries: readonly string[];\n\n try {\n entries = readdirSync(dir);\n } catch {\n return null;\n }\n\n for (const entry of entries) {\n const candidate: string = join(dir, entry);\n\n if (namedPackage(candidate) === name) {\n return candidate;\n }\n }\n\n return null;\n}\n\nfunction sourceOf(workspace: string, name: string): string | null {\n for (const group of PROJECT_DIRS) {\n const found: string | null = sourceInGroup(join(workspace, group), name);\n\n if (found !== null) {\n return found;\n }\n }\n\n return null;\n}\n\n/**\n * Правлены ли ресурсы позже, чем собран пакет, из которого идёт раскладка.\n *\n * `null` значит «сверять не с чем или сверка сошлась»: у потребителя исходников нет вовсе, а у\n * того, кто запускает пакет прямо из исходников, собранное и исходное — один и тот же каталог.\n */\nexport function staleBuild(packageRoot: string, name: string): IStaleBuild | null {\n const workspace: string | null = workspaceOf(packageRoot);\n if (workspace === null) {\n return null;\n }\n const source: string | null = sourceOf(workspace, name);\n if (source === null || source === packageRoot) {\n return null;\n }\n\n const built: INewest | null = newestIn(join(packageRoot, 'assets'));\n const written: INewest | null = newestIn(join(source, 'assets'));\n if (!built || !written || written.at <= built.at) {\n return null;\n }\n\n return { source: join(source, 'assets'), newest: written.path };\n}\n"]}
|
package/lib/hooks-map.d.ts
CHANGED
|
@@ -36,4 +36,34 @@ export declare function boundInSettings(root: string): readonly string[];
|
|
|
36
36
|
* получало бы список из всех гардов разом.
|
|
37
37
|
*/
|
|
38
38
|
export declare function unboundHooks(bindings: readonly IHookBinding[], root: string): readonly IHookBinding[];
|
|
39
|
+
/**
|
|
40
|
+
* Образцы, под которыми гарды стоят в настройке агента: ключ — «событие и путь».
|
|
41
|
+
*
|
|
42
|
+
* Настройку, которую не разобрать, читать здесь нечем: образец лежит полем объекта, и вынуть его
|
|
43
|
+
* из текста поиском значило бы гадать. Тогда карта пуста, и расхождений не находится ни одного —
|
|
44
|
+
* это честнее, чем назвать расхождением то, чего не прочитали.
|
|
45
|
+
*/
|
|
46
|
+
export declare function matchersInSettings(root: string): ReadonlyMap<string, string>;
|
|
47
|
+
/** Гард, чьё объявление разошлось с образцом, под которым его зовут. */
|
|
48
|
+
export interface IMatcherDrift {
|
|
49
|
+
readonly event: string;
|
|
50
|
+
readonly path: string;
|
|
51
|
+
/** Что гард объявляет о себе строкой `# rt-hook:`. */
|
|
52
|
+
readonly declared: string;
|
|
53
|
+
/** Под чем он на самом деле стоит в настройке агента. */
|
|
54
|
+
readonly bound: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Расхождения объявления гарда с образцом, под которым его зовут.
|
|
58
|
+
*
|
|
59
|
+
* Гард, подписанный не на то, что объявляет, хуже неподключённого: снаружи он выглядит
|
|
60
|
+
* работающим — путь его в настройке назван, файл разложен, набор сценариев зелёный, — а вызов,
|
|
61
|
+
* ради которого его тело и написано, до него не доходит никогда. Так гейт правил и разбирал
|
|
62
|
+
* вызовы браузера веткой, которая не исполнялась ни разу: набор звал гард напрямую с
|
|
63
|
+
* подставленным вводом и объявления не читал вовсе.
|
|
64
|
+
*
|
|
65
|
+
* Судятся только подключённые гарды: о неподключённых говорит своя строка, и назвать их дважды
|
|
66
|
+
* значило бы спрятать настоящее расхождение среди повторов.
|
|
67
|
+
*/
|
|
68
|
+
export declare function driftedMatchers(bindings: readonly IHookBinding[], root: string): readonly IMatcherDrift[];
|
|
39
69
|
//# sourceMappingURL=hooks-map.d.ts.map
|
package/lib/hooks-map.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-map.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/hooks-map.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hooks-map.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/hooks-map.ts"],"names":[],"mappings":"AAoBA,qEAAqE;AACrE,eAAO,MAAM,aAAa,EAAE,MAAgC,CAAC;AAI7D,MAAM,WAAW,YAAY;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,YAAY,EAAE,CAM9E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwBvF;AAiDD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAsB/D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,YAAY,EAAE,CAIrG;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAa5E;AAED,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,aAAa,EAAE,CAczG"}
|