@rt-tools/agent-kit 0.3.0 → 0.5.0
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 +194 -30
- package/assets/agents/business-analyst.md +74 -0
- package/assets/agents/project-manager.md +70 -0
- package/assets/agents/qa-engineer.md +72 -0
- package/assets/agents/skill-curator.md +110 -0
- package/assets/agents/spec-critic.md +44 -0
- package/assets/agents/spec-writer.md +50 -0
- package/assets/checks/board.github.mjs +329 -0
- package/assets/checks/check-board.github.mjs +181 -0
- package/assets/checks/check-doc-paths.mjs +163 -0
- package/assets/checks/check-dupes.mjs +277 -0
- package/assets/checks/check-lib-layers.mjs +573 -0
- package/assets/checks/check-reuse.mjs +208 -0
- package/assets/checks/check-schema-drift.mjs +186 -0
- package/assets/checks/check-specs.mjs +1086 -0
- package/assets/checks/check-styles.mjs +109 -0
- package/assets/checks/rt-kit-checks.config.mjs +134 -0
- package/assets/checks/task-new.github.mjs +198 -0
- package/assets/commands/skill-curator.md +70 -0
- package/assets/defaults/gate-map.sh +106 -0
- package/assets/defaults/project.sh +204 -0
- package/assets/hooks/browser-device-id.sh +0 -0
- package/assets/hooks/browser-guard-device-id.sh +2 -1
- package/assets/hooks/browser-guard-no-asking.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +2 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
- package/assets/hooks/browser-guard-require-select.sh +2 -1
- package/assets/hooks/commit-msg.sh +1 -1
- package/assets/hooks/constitution-index.sh +5 -4
- package/assets/hooks/dev-server-guard.sh +8 -6
- package/assets/hooks/docs-guard.sh +223 -37
- package/assets/hooks/git-guard-delivery.sh +171 -31
- package/assets/hooks/git-guard-main.sh +1 -0
- package/assets/hooks/git-guard-push-tests.sh +34 -13
- package/assets/hooks/glossary-load.sh +23 -0
- package/assets/hooks/grill-gate.sh +96 -0
- package/assets/hooks/lint-after-edit.sh +155 -30
- package/assets/hooks/qa-dataid-guard.sh +72 -32
- package/assets/hooks/reuse-first-guard.sh +105 -34
- package/assets/hooks/skill-gate-rearm.sh +1 -0
- package/assets/hooks/skill-gate.sh +75 -15
- package/assets/hooks/skill-loaded.sh +1 -0
- package/assets/hooks/sql-guard.sh +606 -56
- package/assets/hooks/task-context-load.sh +100 -0
- package/assets/hooks/task-flow-guard.sh +118 -0
- package/assets/laws/{access.md → application/access.md} +1 -4
- package/assets/laws/{locales.md → application/locales.md} +1 -3
- package/assets/laws/application/money.md +41 -0
- package/assets/laws/application/ownership.md +32 -0
- package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
- package/assets/laws/code-structure.md +7 -6
- package/assets/laws/delivery.md +53 -3
- package/assets/laws/entity-editing.md +49 -55
- package/assets/laws/entity-models.md +4 -14
- package/assets/laws/frontend-application.md +5 -5
- package/assets/laws/lib-imports.md +14 -1
- package/assets/laws/lists.md +33 -0
- package/assets/laws/navigation.md +40 -0
- package/assets/laws/project-documentation.md +27 -8
- package/assets/laws/reuse-first.md +26 -21
- package/assets/laws/shared-code.md +13 -1
- package/assets/laws/verifiability.md +30 -1
- package/assets/laws/work-conduct.md +59 -0
- package/assets/patterns/admin-lists-screen.md +131 -0
- package/assets/patterns/admin-nav-item.md +71 -0
- package/assets/patterns/angular-patterns-state.md +29 -22
- package/assets/patterns/api-layer-pair.md +40 -30
- package/assets/patterns/browser-verification-measure.md +41 -38
- package/assets/patterns/browser-verification-stand.md +106 -42
- package/assets/patterns/component-structure-new.md +33 -32
- package/assets/patterns/dependencies-upgrade.md +65 -0
- package/assets/patterns/doc-style-sweep.md +65 -28
- package/assets/patterns/doc-style-write.md +36 -33
- package/assets/patterns/entity-aside.md +136 -0
- package/assets/patterns/entity-models-new.md +124 -0
- package/assets/patterns/entity-store.md +91 -0
- package/assets/patterns/git-workflow-commit.azure.md +259 -0
- package/assets/patterns/git-workflow-commit.github.md +337 -0
- package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
- package/assets/patterns/git-workflow-merge.md +42 -25
- package/assets/patterns/git-workflow-migration.md +61 -31
- package/assets/patterns/git-workflow-restart.md +20 -20
- package/assets/patterns/lib-layers-move.md +50 -32
- package/assets/patterns/lib-layers-new.md +41 -29
- package/assets/patterns/ownership-scope-resolve.md +69 -0
- package/assets/patterns/permissions-procedure.md +35 -33
- package/assets/patterns/platform-access-di.md +39 -25
- package/assets/patterns/pricing-quote.md +71 -0
- package/assets/patterns/reuse-first-extend.md +22 -22
- package/assets/patterns/seo-page.md +52 -40
- package/assets/patterns/seo-verify.md +48 -29
- package/assets/patterns/shared-code-new.md +37 -31
- package/assets/patterns/spec-driven-domain.md +60 -37
- package/assets/patterns/spec-driven-rule.md +55 -40
- package/assets/patterns/styling-bem-component.md +43 -32
- package/assets/patterns/styling-bem-layout.md +30 -24
- package/assets/patterns/task-flow-close.md +154 -0
- package/assets/patterns/task-flow-resume.md +94 -0
- package/assets/patterns/task-flow-start.md +129 -0
- package/assets/patterns/testing-e2e.md +53 -51
- package/assets/patterns/testing-unit.md +70 -46
- package/assets/patterns/translations-key.md +32 -19
- package/assets/patterns/ts-procedure.md +24 -25
- package/assets/rules/angular-patterns.md +50 -27
- package/assets/rules/api-layer.md +46 -28
- package/assets/rules/browser-verification.md +67 -48
- package/assets/rules/component-structure.md +43 -27
- package/assets/rules/dependencies.md +66 -0
- package/assets/rules/doc-style.md +95 -39
- package/assets/rules/entity-conventions.md +78 -0
- package/assets/rules/entity-models.md +70 -0
- package/assets/rules/git-workflow.azure.md +116 -0
- package/assets/rules/git-workflow.github.md +123 -0
- package/assets/rules/git-workflow.gitlab.md +113 -0
- package/assets/rules/lib-layers.md +56 -30
- package/assets/rules/lists.md +73 -0
- package/assets/rules/navigation.md +78 -0
- package/assets/rules/ownership-scope.md +63 -0
- package/assets/rules/permissions.md +43 -25
- package/assets/rules/platform-access.md +57 -29
- package/assets/rules/pricing.md +64 -0
- package/assets/rules/reuse-first.md +57 -43
- package/assets/rules/seo.md +51 -30
- package/assets/rules/shared-code.md +51 -26
- package/assets/rules/spec-driven.md +107 -51
- package/assets/rules/styling-bem.md +54 -39
- package/assets/rules/task-flow.md +150 -0
- package/assets/rules/testing.md +78 -47
- package/assets/rules/translations.md +48 -31
- package/assets/rules/typescript-conventions.md +57 -27
- package/assets/skills/agent-kit.md +85 -0
- package/assets/skills/write-a-skill.md +108 -0
- package/assets/templates/gate-map.sh +23 -15
- package/assets/templates/implementation.md +14 -8
- package/assets/templates/pattern.md +1 -1
- package/assets/templates/project.sh +32 -19
- package/assets/templates/rule.md +2 -2
- package/assets/variants.json +20 -0
- package/assets/workflows/feature.js +134 -0
- package/assets/workflows/plan.js +150 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +78 -5
- package/bin/agent-kit.js.map +1 -1
- package/bin/prompt.d.ts +5 -0
- package/bin/prompt.d.ts.map +1 -1
- package/bin/prompt.js +19 -7
- package/bin/prompt.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/assets.d.ts +8 -3
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +13 -3
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +52 -5
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +104 -16
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +22 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +202 -14
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +5 -1
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +29 -2
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +26 -9
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +41 -15
- package/lib/config.js.map +1 -1
- package/lib/freshness.d.ts +14 -0
- package/lib/freshness.d.ts.map +1 -0
- package/lib/freshness.js +116 -0
- package/lib/freshness.js.map +1 -0
- package/lib/hooks-map.d.ts +27 -0
- package/lib/hooks-map.d.ts.map +1 -0
- package/lib/hooks-map.js +77 -0
- package/lib/hooks-map.js.map +1 -0
- package/lib/integrity.d.ts +36 -0
- package/lib/integrity.d.ts.map +1 -0
- package/lib/integrity.js +44 -0
- package/lib/integrity.js.map +1 -0
- package/lib/picker.d.ts +11 -1
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +44 -6
- package/lib/picker.js.map +1 -1
- package/lib/sync.d.ts +26 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -4
- package/lib/sync.js.map +1 -1
- package/lib/variants.d.ts +44 -0
- package/lib/variants.d.ts.map +1 -0
- package/lib/variants.js +82 -0
- package/lib/variants.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.0.tgz +0 -0
- package/assets/laws/admin-lists.md +0 -35
- package/assets/laws/admin-navigation.md +0 -38
- package/assets/patterns/git-workflow-commit.md +0 -175
- package/assets/rules/git-workflow.md +0 -106
- package/rt-tools-agent-kit-0.3.0.tgz +0 -0
package/lib/catalog.js
CHANGED
|
@@ -5,25 +5,52 @@
|
|
|
5
5
|
* `init` спрашивает, какие законы взять, а `list` показывает и то, от чего проект отказался.
|
|
6
6
|
* И там и там нужен полный набор, а `collectAssets` по устройству отдаёт уже отобранный.
|
|
7
7
|
*/
|
|
8
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { accessSync, constants, readdirSync, readFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { KINDS } from './config.js';
|
|
11
|
+
import { matchesVariant, readAxes, variantOf, withoutVariant } from './variants.js';
|
|
11
12
|
const TITLE = /^#\s+(\S.*)$/m;
|
|
13
|
+
/** Запускается ли файл сам по себе. Спрашиваем систему, а не разбираем биты режима руками. */
|
|
14
|
+
function isExecutable(path) {
|
|
15
|
+
try {
|
|
16
|
+
accessSync(path, constants.X_OK);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
/** Заголовок нужен человеку, который выбирает: имена законов ему ни о чём не говорят. */
|
|
13
24
|
export function titleOf(text, fallback) {
|
|
14
25
|
const found = text.match(TITLE);
|
|
15
26
|
return found ? found[1].trim() : fallback;
|
|
16
27
|
}
|
|
17
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Файлы рода — вместе с теми, что лежат в подкаталогах.
|
|
30
|
+
*
|
|
31
|
+
* Обход рекурсивный потому, что у законов два слоя: общий закон верен любому приложению этого
|
|
32
|
+
* класса, а закон приложения — только такому, где есть деньги, локали или владеющая сущность.
|
|
33
|
+
* Слой отражён каталогом, и путь внутри рода едет в идентификатор целиком, чтобы разложенное
|
|
34
|
+
* повторяло раскладку пакета.
|
|
35
|
+
*/
|
|
36
|
+
function filesOf(dir, prefix = '') {
|
|
37
|
+
let entries;
|
|
18
38
|
try {
|
|
19
|
-
|
|
20
|
-
.filter((entry) => entry.isFile())
|
|
21
|
-
.map((entry) => entry.name)
|
|
22
|
-
.sort();
|
|
39
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
23
40
|
}
|
|
24
41
|
catch {
|
|
25
42
|
return [];
|
|
26
43
|
}
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const entry of [...entries].sort((one, two) => one.name.localeCompare(two.name))) {
|
|
46
|
+
if (entry.isFile()) {
|
|
47
|
+
files.push(`${prefix}${entry.name}`);
|
|
48
|
+
}
|
|
49
|
+
else if (entry.isDirectory()) {
|
|
50
|
+
files.push(...filesOf(join(dir, entry.name), `${prefix}${entry.name}/`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return files;
|
|
27
54
|
}
|
|
28
55
|
/**
|
|
29
56
|
* Всё, что лежит в ресурсах пакета. Список не выписан руками, а читается из каталога:
|
|
@@ -31,12 +58,16 @@ function filesOf(dir) {
|
|
|
31
58
|
* нечем — ресурс просто не приезжал бы к проекту.
|
|
32
59
|
*/
|
|
33
60
|
export function readCatalog(assetsDir) {
|
|
61
|
+
const axes = readAxes(assetsDir);
|
|
34
62
|
const entries = [];
|
|
35
63
|
for (const kind of KINDS) {
|
|
36
64
|
for (const file of filesOf(join(assetsDir, kind))) {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
65
|
+
const path = join(assetsDir, kind, file);
|
|
66
|
+
const text = readFileSync(path, 'utf8');
|
|
67
|
+
const variant = variantOf(file, axes);
|
|
68
|
+
const name = withoutVariant(file, variant).replace(/\.[^./]+$/, '');
|
|
69
|
+
const executable = isExecutable(path);
|
|
70
|
+
entries.push({ id: `${kind}/${file}`, kind, name, title: titleOf(text, name), variant, text, executable });
|
|
40
71
|
}
|
|
41
72
|
}
|
|
42
73
|
return entries;
|
|
@@ -51,21 +82,78 @@ export function readCatalog(assetsDir) {
|
|
|
51
82
|
*
|
|
52
83
|
* `skip` вычитает из выбранного. Отказ от одного закона не должен требовать переписать
|
|
53
84
|
* список из пятнадцати, а точечное исключение остаётся точечным.
|
|
85
|
+
*
|
|
86
|
+
* Вид отбирается раньше обоих: ресурс чужого хостинга не «не выбран» — его в этом дереве не
|
|
87
|
+
* существует вовсе, и называть его в `skip` проекту незачем.
|
|
54
88
|
*/
|
|
55
|
-
export function isChosen(entry,
|
|
56
|
-
if (
|
|
89
|
+
export function isChosen(entry, selection) {
|
|
90
|
+
if (!matchesVariant(entry.variant, selection.variants)) {
|
|
57
91
|
return false;
|
|
58
92
|
}
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
if (selection.skip.includes(entry.id)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const restricted = selection.only.some((id) => id.startsWith(`${entry.kind}/`));
|
|
97
|
+
return !restricted || selection.only.includes(entry.id);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Ресурсы, оставшиеся без вида под выбор дерева.
|
|
101
|
+
*
|
|
102
|
+
* Молчаливый пропуск такого ресурса — худший из исходов. Правило поставки едет тремя видами, а
|
|
103
|
+
* команды заведения задачи и сверки очереди — одним: дерево, выбравшее чужой хостинг, получает
|
|
104
|
+
* правило, которое зовёт три команды, и ни одного скрипта под ними. Отказа при этом не бывает —
|
|
105
|
+
* ресурс просто не приезжает, и узнают об этом, когда команда из правила не находится.
|
|
106
|
+
*
|
|
107
|
+
* Отсюда правило: **ресурс с видами, ни один из которых не совпал, — отказ, а не пропуск.**
|
|
108
|
+
* Снимается он двумя способами, и оба явные: завести недостающий вид в пакете либо назвать
|
|
109
|
+
* существующие в `skip` — дерево тем самым говорит, что обходится без этого ресурса.
|
|
110
|
+
*/
|
|
111
|
+
export function variantGaps(catalog, selection) {
|
|
112
|
+
const groups = new Map();
|
|
113
|
+
for (const entry of catalog) {
|
|
114
|
+
if (entry.variant === null) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const key = `${entry.kind}/${entry.name}`;
|
|
118
|
+
groups.set(key, [...(groups.get(key) ?? []), entry]);
|
|
119
|
+
}
|
|
120
|
+
const gaps = [];
|
|
121
|
+
for (const entries of groups.values()) {
|
|
122
|
+
const axis = entries[0].variant?.axis ?? '';
|
|
123
|
+
if (entries.some((entry) => matchesVariant(entry.variant, selection.variants))) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Отказ от ресурса — законный ответ, и повторять его отказом раскладки незачем: дерево
|
|
127
|
+
// уже сказало, что обходится без него.
|
|
128
|
+
if (entries.every((entry) => selection.skip.includes(entry.id))) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Род, суженный через `only`, отбирает ресурсы поимённо: не названный в нём ресурс не
|
|
132
|
+
// пропал — его не просили.
|
|
133
|
+
const restricted = selection.only.some((id) => id.startsWith(`${entries[0].kind}/`));
|
|
134
|
+
if (restricted && !entries.some((entry) => selection.only.includes(entry.id))) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
gaps.push({
|
|
138
|
+
kind: entries[0].kind,
|
|
139
|
+
name: entries[0].name,
|
|
140
|
+
axis,
|
|
141
|
+
chosen: selection.variants[axis] ?? '',
|
|
142
|
+
available: entries.map((entry) => entry.variant?.value ?? ''),
|
|
143
|
+
ids: entries.map((entry) => entry.id),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return gaps;
|
|
61
147
|
}
|
|
62
148
|
/**
|
|
63
|
-
* Идентификатор по тому, как ресурс назвали в строке запуска. Принимаются все
|
|
64
|
-
*
|
|
149
|
+
* Идентификатор по тому, как ресурс назвали в строке запуска. Принимаются все формы, которыми
|
|
150
|
+
* его называют вслух: `money`, `application/money`, `money.md` и `laws/application/money.md`.
|
|
65
151
|
*/
|
|
66
152
|
export function idOf(spoken, kind, catalog) {
|
|
67
153
|
const wanted = spoken.trim();
|
|
68
|
-
const
|
|
154
|
+
const shortOf = (entry) => entry.name.split('/').pop() ?? entry.name;
|
|
155
|
+
const found = catalog.find((entry) => entry.kind === kind &&
|
|
156
|
+
(entry.id === wanted || entry.name === wanted || shortOf(entry) === wanted || entry.id.endsWith(`/${wanted}`)));
|
|
69
157
|
return found ? found.id : null;
|
|
70
158
|
}
|
|
71
159
|
/** Разбор `--laws access,delivery`: имя без ответа — промах, и молчать о нём нельзя. */
|
package/lib/catalog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Что пакет везёт — прочитанное с диска, до всякого выбора проекта.\n *\n * Каталог нужен отдельно от раскладки, потому что выбирать приходится раньше, чем раскладывать:\n * `init` спрашивает, какие законы взять, а `list` показывает и то, от чего проект отказался.\n * И там и там нужен полный набор, а `collectAssets` по устройству отдаёт уже отобранный.\n */\nimport { Dirent, readdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { KINDS, TKind } from './config.js';\n\nexport interface IEntryOfCatalog {\n /** Идентификатор: `laws/delivery.md`. Он же путь надстройки и ключ в `only` и `skip`. */\n readonly id: string;\n readonly kind: TKind;\n /** Имя без рода и расширения: `delivery`. Им ресурс называют в строке запуска. */\n readonly name: string;\n /** Заголовок первой строки файла; у файла без заголовка — его имя. */\n readonly title: string;\n readonly text: string;\n}\n\nconst TITLE: RegExp = /^#\\s+(\\S.*)$/m;\n\n/** Заголовок нужен человеку, который выбирает: имена законов ему ни о чём не говорят. */\nexport function titleOf(text: string, fallback: string): string {\n const found: RegExpMatchArray | null = text.match(TITLE);\n\n return found ? found[1].trim() : fallback;\n}\n\nfunction filesOf(dir: string): readonly string[] {\n try {\n return readdirSync(dir, { withFileTypes: true })\n .filter((entry: Dirent): boolean => entry.isFile())\n .map((entry: Dirent): string => entry.name)\n .sort();\n } catch {\n return [];\n }\n}\n\n/**\n * Всё, что лежит в ресурсах пакета. Список не выписан руками, а читается из каталога:\n * выписанный разошёлся бы с ним на первом же добавленном законе, и заметить это было бы\n * нечем — ресурс просто не приезжал бы к проекту.\n */\nexport function readCatalog(assetsDir: string): readonly IEntryOfCatalog[] {\n const entries: IEntryOfCatalog[] = [];\n for (const kind of KINDS) {\n for (const file of filesOf(join(assetsDir, kind))) {\n const text: string = readFileSync(join(assetsDir, kind, file), 'utf8');\n const name: string = file.replace(/\\.[^.]+$/, '');\n entries.push({ id: `${kind}/${file}`, kind, name, title: titleOf(text, name), text });\n }\n }\n\n return entries;\n}\n\n/**\n * Взят ли ресурс при этом выборе.\n *\n * `only` ограничивает **только те роды, которые сам называет**. Проект, выбравший девять\n * законов из пятнадцати, шаблоны при этом не терял бы: перечисляя законы, он говорит о\n * законах, а не обо всём, что пакет везёт. Обратное правило читалось бы как «выбрал законы —\n * остался без шаблонов», и заметил бы это только тот, кто пошёл за шаблоном.\n *\n * `skip` вычитает из выбранного. Отказ от одного закона не должен требовать переписать\n * список из пятнадцати, а точечное исключение остаётся точечным.\n */\nexport function isChosen(entry: IEntryOfCatalog, only: readonly string[], skip: readonly string[]): boolean {\n if (skip.includes(entry.id)) {\n return false;\n }\n const restricted: boolean = only.some((id: string): boolean => id.startsWith(`${entry.kind}/`));\n\n return !restricted || only.includes(entry.id);\n}\n\n/**\n * Идентификатор по тому, как ресурс назвали в строке запуска. Принимаются все три формы,\n * которыми его называют вслух: `access`, `access.md` и `laws/access.md`.\n */\nexport function idOf(spoken: string, kind: TKind, catalog: readonly IEntryOfCatalog[]): string | null {\n const wanted: string = spoken.trim();\n const found: IEntryOfCatalog | undefined = catalog.find(\n (entry: IEntryOfCatalog): boolean =>\n entry.kind === kind && (entry.id === wanted || entry.name === wanted || entry.id.endsWith(`/${wanted}`))\n );\n\n return found ? found.id : null;\n}\n\nexport interface IResolvedSelection {\n readonly ids: readonly string[];\n /** Имена, которым в каталоге ничего не отвечает: промах в строке запуска, а не выбор. */\n readonly unknown: readonly string[];\n}\n\n/** Разбор `--laws access,delivery`: имя без ответа — промах, и молчать о нём нельзя. */\nexport function resolveSelection(spoken: readonly string[], kind: TKind, catalog: readonly IEntryOfCatalog[]): IResolvedSelection {\n const ids: string[] = [];\n const unknown: string[] = [];\n\n for (const name of spoken) {\n const id: string | null = idOf(name, kind, catalog);\n if (id === null) {\n unknown.push(name.trim());\n } else if (!ids.includes(id)) {\n ids.push(id);\n }\n }\n\n return { ids, unknown };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Что пакет везёт — прочитанное с диска, до всякого выбора проекта.\n *\n * Каталог нужен отдельно от раскладки, потому что выбирать приходится раньше, чем раскладывать:\n * `init` спрашивает, какие законы взять, а `list` показывает и то, от чего проект отказался.\n * И там и там нужен полный набор, а `collectAssets` по устройству отдаёт уже отобранный.\n */\nimport { accessSync, constants, Dirent, readdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { KINDS, TKind } from './config.js';\nimport { IAxis, IVariant, matchesVariant, readAxes, variantOf, withoutVariant } from './variants.js';\n\nexport interface IEntryOfCatalog {\n /** Идентификатор: `laws/application/money.md`. Он же путь надстройки и ключ в `only` и `skip`. */\n readonly id: string;\n readonly kind: TKind;\n /**\n * Имя без рода, вида и расширения: `delivery`, `application/money`. Им ресурс называют в\n * строке запуска, и им же он ложится в дерево проекта.\n */\n readonly name: string;\n /** Заголовок первой строки файла; у файла без заголовка — его имя. */\n readonly title: string;\n /** Вид ресурса, если он лежит в пакете в нескольких: `git-workflow.github.md`. */\n readonly variant: IVariant | null;\n readonly text: string;\n /**\n * Запускается ли файл сам по себе. Гард зовут по пути, а не через оболочку, и файл без\n * права на запуск отвечает отказом доступа — то есть ненулевым кодом, который читается как\n * «правка отбита». Признак берётся с файла в пакете и переносится на разложенный.\n */\n readonly executable: boolean;\n}\n\nconst TITLE: RegExp = /^#\\s+(\\S.*)$/m;\n\n/** Запускается ли файл сам по себе. Спрашиваем систему, а не разбираем биты режима руками. */\nfunction isExecutable(path: string): boolean {\n try {\n accessSync(path, constants.X_OK);\n\n return true;\n } catch {\n return false;\n }\n}\n\n/** Заголовок нужен человеку, который выбирает: имена законов ему ни о чём не говорят. */\nexport function titleOf(text: string, fallback: string): string {\n const found: RegExpMatchArray | null = text.match(TITLE);\n\n return found ? found[1].trim() : fallback;\n}\n\n/**\n * Файлы рода — вместе с теми, что лежат в подкаталогах.\n *\n * Обход рекурсивный потому, что у законов два слоя: общий закон верен любому приложению этого\n * класса, а закон приложения — только такому, где есть деньги, локали или владеющая сущность.\n * Слой отражён каталогом, и путь внутри рода едет в идентификатор целиком, чтобы разложенное\n * повторяло раскладку пакета.\n */\nfunction filesOf(dir: string, prefix: string = ''): readonly string[] {\n let entries: readonly Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n\n const files: string[] = [];\n for (const entry of [...entries].sort((one: Dirent, two: Dirent): number => one.name.localeCompare(two.name))) {\n if (entry.isFile()) {\n files.push(`${prefix}${entry.name}`);\n } else if (entry.isDirectory()) {\n files.push(...filesOf(join(dir, entry.name), `${prefix}${entry.name}/`));\n }\n }\n\n return files;\n}\n\n/**\n * Всё, что лежит в ресурсах пакета. Список не выписан руками, а читается из каталога:\n * выписанный разошёлся бы с ним на первом же добавленном законе, и заметить это было бы\n * нечем — ресурс просто не приезжал бы к проекту.\n */\nexport function readCatalog(assetsDir: string): readonly IEntryOfCatalog[] {\n const axes: readonly IAxis[] = readAxes(assetsDir);\n const entries: IEntryOfCatalog[] = [];\n\n for (const kind of KINDS) {\n for (const file of filesOf(join(assetsDir, kind))) {\n const path: string = join(assetsDir, kind, file);\n const text: string = readFileSync(path, 'utf8');\n const variant: IVariant | null = variantOf(file, axes);\n const name: string = withoutVariant(file, variant).replace(/\\.[^./]+$/, '');\n const executable: boolean = isExecutable(path);\n entries.push({ id: `${kind}/${file}`, kind, name, title: titleOf(text, name), variant, text, executable });\n }\n }\n\n return entries;\n}\n\n/** Чем проект ограничил раскладку: выбором, отказом и видами, которые он назвал. */\nexport interface ISelection {\n readonly only: readonly string[];\n readonly skip: readonly string[];\n readonly variants: Readonly<Record<string, string>>;\n}\n\n/**\n * Взят ли ресурс при этом выборе.\n *\n * `only` ограничивает **только те роды, которые сам называет**. Проект, выбравший девять\n * законов из пятнадцати, шаблоны при этом не терял бы: перечисляя законы, он говорит о\n * законах, а не обо всём, что пакет везёт. Обратное правило читалось бы как «выбрал законы —\n * остался без шаблонов», и заметил бы это только тот, кто пошёл за шаблоном.\n *\n * `skip` вычитает из выбранного. Отказ от одного закона не должен требовать переписать\n * список из пятнадцати, а точечное исключение остаётся точечным.\n *\n * Вид отбирается раньше обоих: ресурс чужого хостинга не «не выбран» — его в этом дереве не\n * существует вовсе, и называть его в `skip` проекту незачем.\n */\nexport function isChosen(entry: IEntryOfCatalog, selection: ISelection): boolean {\n if (!matchesVariant(entry.variant, selection.variants)) {\n return false;\n }\n if (selection.skip.includes(entry.id)) {\n return false;\n }\n const restricted: boolean = selection.only.some((id: string): boolean => id.startsWith(`${entry.kind}/`));\n\n return !restricted || selection.only.includes(entry.id);\n}\n\n/** Ресурс, у которого виды есть, но ни один не отвечает тому, что выбрало дерево. */\nexport interface IGapOfVariant {\n readonly kind: TKind;\n /** Имя без вида — под ним ресурс лёг бы в дерево. */\n readonly name: string;\n readonly axis: string;\n /** Что выбрало дерево по этой оси. */\n readonly chosen: string;\n /** Виды, которые у ресурса есть на самом деле. */\n readonly available: readonly string[];\n /** Идентификаторы этих видов: ими же пробел и снимается через `skip`. */\n readonly ids: readonly string[];\n}\n\n/**\n * Ресурсы, оставшиеся без вида под выбор дерева.\n *\n * Молчаливый пропуск такого ресурса — худший из исходов. Правило поставки едет тремя видами, а\n * команды заведения задачи и сверки очереди — одним: дерево, выбравшее чужой хостинг, получает\n * правило, которое зовёт три команды, и ни одного скрипта под ними. Отказа при этом не бывает —\n * ресурс просто не приезжает, и узнают об этом, когда команда из правила не находится.\n *\n * Отсюда правило: **ресурс с видами, ни один из которых не совпал, — отказ, а не пропуск.**\n * Снимается он двумя способами, и оба явные: завести недостающий вид в пакете либо назвать\n * существующие в `skip` — дерево тем самым говорит, что обходится без этого ресурса.\n */\nexport function variantGaps(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IGapOfVariant[] {\n const groups: Map<string, IEntryOfCatalog[]> = new Map();\n for (const entry of catalog) {\n if (entry.variant === null) {\n continue;\n }\n const key: string = `${entry.kind}/${entry.name}`;\n groups.set(key, [...(groups.get(key) ?? []), entry]);\n }\n\n const gaps: IGapOfVariant[] = [];\n for (const entries of groups.values()) {\n const axis: string = entries[0].variant?.axis ?? '';\n if (entries.some((entry: IEntryOfCatalog): boolean => matchesVariant(entry.variant, selection.variants))) {\n continue;\n }\n // Отказ от ресурса — законный ответ, и повторять его отказом раскладки незачем: дерево\n // уже сказало, что обходится без него.\n if (entries.every((entry: IEntryOfCatalog): boolean => selection.skip.includes(entry.id))) {\n continue;\n }\n // Род, суженный через `only`, отбирает ресурсы поимённо: не названный в нём ресурс не\n // пропал — его не просили.\n const restricted: boolean = selection.only.some((id: string): boolean => id.startsWith(`${entries[0].kind}/`));\n if (restricted && !entries.some((entry: IEntryOfCatalog): boolean => selection.only.includes(entry.id))) {\n continue;\n }\n gaps.push({\n kind: entries[0].kind,\n name: entries[0].name,\n axis,\n chosen: selection.variants[axis] ?? '',\n available: entries.map((entry: IEntryOfCatalog): string => entry.variant?.value ?? ''),\n ids: entries.map((entry: IEntryOfCatalog): string => entry.id),\n });\n }\n\n return gaps;\n}\n\n/**\n * Идентификатор по тому, как ресурс назвали в строке запуска. Принимаются все формы, которыми\n * его называют вслух: `money`, `application/money`, `money.md` и `laws/application/money.md`.\n */\nexport function idOf(spoken: string, kind: TKind, catalog: readonly IEntryOfCatalog[]): string | null {\n const wanted: string = spoken.trim();\n const shortOf: (entry: IEntryOfCatalog) => string = (entry: IEntryOfCatalog): string => entry.name.split('/').pop() ?? entry.name;\n const found: IEntryOfCatalog | undefined = catalog.find(\n (entry: IEntryOfCatalog): boolean =>\n entry.kind === kind &&\n (entry.id === wanted || entry.name === wanted || shortOf(entry) === wanted || entry.id.endsWith(`/${wanted}`))\n );\n\n return found ? found.id : null;\n}\n\nexport interface IResolvedSelection {\n readonly ids: readonly string[];\n /** Имена, которым в каталоге ничего не отвечает: промах в строке запуска, а не выбор. */\n readonly unknown: readonly string[];\n}\n\n/** Разбор `--laws access,delivery`: имя без ответа — промах, и молчать о нём нельзя. */\nexport function resolveSelection(spoken: readonly string[], kind: TKind, catalog: readonly IEntryOfCatalog[]): IResolvedSelection {\n const ids: string[] = [];\n const unknown: string[] = [];\n\n for (const name of spoken) {\n const id: string | null = idOf(name, kind, catalog);\n if (id === null) {\n unknown.push(name.trim());\n } else if (!ids.includes(id)) {\n ids.push(id);\n }\n }\n\n return { ids, unknown };\n}\n"]}
|
package/lib/commands.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IStaleBuild } from './freshness.js';
|
|
1
2
|
export interface IOutcomeOfCommand {
|
|
2
3
|
readonly code: number;
|
|
3
4
|
readonly lines: readonly string[];
|
|
@@ -7,14 +8,34 @@ export interface IEnvironment {
|
|
|
7
8
|
readonly root: string;
|
|
8
9
|
readonly version: string;
|
|
9
10
|
readonly assetsDir: string;
|
|
11
|
+
/**
|
|
12
|
+
* Ресурсы правлены позже, чем собран пакет, из которого идёт раскладка; `null` — сверять не с
|
|
13
|
+
* чем. Считается на краю, где пакет знает своё расположение: библиотечные модули работают с
|
|
14
|
+
* тем каталогом ресурсов, который им дали, и о существовании исходников не знают.
|
|
15
|
+
*/
|
|
16
|
+
readonly stale?: IStaleBuild | null;
|
|
10
17
|
}
|
|
18
|
+
/** Куда уезжает прежнее содержимое файла, отданного пакету. */
|
|
19
|
+
export declare const KEPT_SUFFIX: string;
|
|
11
20
|
/**
|
|
12
21
|
* Что проект выбрал при заведении конфига. Пустой список — весь набор; выбор приходит сюда
|
|
13
22
|
* готовым, потому что взять его негде, кроме края: у строки запуска флаг, у терминала вопрос,
|
|
14
23
|
* а у неинтерактивного прогона нет ни того ни другого — и решать это команде не по чину.
|
|
15
24
|
*/
|
|
16
|
-
export declare function init(root: string, only?: readonly string[]): IOutcomeOfCommand;
|
|
25
|
+
export declare function init(root: string, only?: readonly string[], variants?: Readonly<Record<string, string>>, assetsDir?: string): IOutcomeOfCommand;
|
|
17
26
|
export declare function sync(env: IEnvironment, check: boolean): IOutcomeOfCommand;
|
|
27
|
+
/**
|
|
28
|
+
* Отдать пакету файл, который лежит в дереве не от него.
|
|
29
|
+
*
|
|
30
|
+
* Пока команды не было, `sync` отказывал на такой файл поимённо и предлагал «перенести правку в
|
|
31
|
+
* надстройку или снять» — то есть работу, которую делают руками, и до неё же сводилась вся
|
|
32
|
+
* установка в живое дерево. При этом один чужой файл останавливал раскладку целиком.
|
|
33
|
+
*
|
|
34
|
+
* Молчаливой перезаписи здесь нет и быть не может: в файле лежит то, чего в дереве больше нигде
|
|
35
|
+
* нет. Поэтому переход — это переименование прежнего содержимого рядом и повторный `sync`:
|
|
36
|
+
* пакет кладёт своё, а прежнее остаётся на диске под именем с пометкой и ждёт разбора.
|
|
37
|
+
*/
|
|
38
|
+
export declare function adopt(env: IEnvironment, names: readonly string[]): IOutcomeOfCommand;
|
|
18
39
|
export declare function doctor(env: IEnvironment): IOutcomeOfCommand;
|
|
19
40
|
/**
|
|
20
41
|
* Что пакет везёт и что из этого взято здесь.
|
package/lib/commands.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/commands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/commands.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAO7C,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACvC;AAoDD,+DAA+D;AAC/D,eAAO,MAAM,WAAW,EAAE,MAAyB,CAAC;AAyFpD;;;;GAIG;AACH,wBAAgB,IAAI,CAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,SAAS,MAAM,EAAO,EAC5B,QAAQ,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,EAC/C,SAAS,GAAE,MAAW,GACvB,iBAAiB,CAgCnB;AAgBD,wBAAgB,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,iBAAiB,CA2EzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,CA+CpF;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CA2C3D;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAkDzD"}
|
package/lib/commands.js
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
* а `process.exit` зовёт только точка входа — иначе ни одну из них нельзя было бы проверить
|
|
4
4
|
* спекой, не перехватывая поток вывода.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { collectAssets } from './assets.js';
|
|
9
9
|
import { isChosen, readCatalog } from './catalog.js';
|
|
10
10
|
import { isUnfilled } from './companion.js';
|
|
11
11
|
import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, readConfig } from './config.js';
|
|
12
|
+
import { hooksSection, SETTINGS_PATH } from './hooks-map.js';
|
|
12
13
|
import { isRefusal } from './plan.js';
|
|
13
14
|
import { pendingOf, planSync, runSync } from './sync.js';
|
|
15
|
+
import { placeholdersOf } from './vars.js';
|
|
16
|
+
import { readAxes, unansweredAxes } from './variants.js';
|
|
14
17
|
/** Что с файлом сделала раскладка. Прошедшее время здесь правда: `sync` уже записал. */
|
|
15
18
|
const DONE_WORD = {
|
|
16
19
|
create: 'положен',
|
|
@@ -33,11 +36,15 @@ const STATE_WORD = {
|
|
|
33
36
|
};
|
|
34
37
|
const NOT_CHOSEN = 'не выбран';
|
|
35
38
|
const SKIPPED = 'пропущен';
|
|
39
|
+
/** Ресурс чужого вида: в этом дереве его не существует, а не «от него отказались». */
|
|
40
|
+
const OTHER_VARIANT = 'другой вид';
|
|
36
41
|
const KIND_TITLE = {
|
|
37
42
|
laws: 'ЗАКОНЫ',
|
|
38
43
|
rules: 'ПРАВИЛА',
|
|
39
44
|
patterns: 'ПАТТЕРНЫ',
|
|
45
|
+
skills: 'СКИЛЫ',
|
|
40
46
|
hooks: 'ХУКИ',
|
|
47
|
+
defaults: 'УМОЛЧАНИЯ',
|
|
41
48
|
checks: 'ПРОВЕРКИ',
|
|
42
49
|
agents: 'АГЕНТЫ',
|
|
43
50
|
commands: 'КОМАНДЫ',
|
|
@@ -51,56 +58,152 @@ const COMPANION_WORD = {
|
|
|
51
58
|
filled: 'заполнен',
|
|
52
59
|
};
|
|
53
60
|
const NO_CONFIG = `нет \`${CONFIG_PATH}\` — начни с \`agent-kit init\``;
|
|
61
|
+
/** Куда уезжает прежнее содержимое файла, отданного пакету. */
|
|
62
|
+
export const KEPT_SUFFIX = '.before-rt-kit';
|
|
54
63
|
const holes = (result) => [...result.missing].map(([asset, names]) => ` ${asset}: нет значений для ${names.map((name) => `{{${name}}}`).join(', ')}`);
|
|
55
64
|
const unfilled = (result) => result.companions.filter(isUnfilled);
|
|
65
|
+
/** Ось без ответа: чем её спрашивают и из чего выбирают. */
|
|
66
|
+
const axisLines = (axes) => axes.flatMap((axis) => [
|
|
67
|
+
` ${axis.name} — ${axis.question}`,
|
|
68
|
+
...axis.options.map((option) => ` ${option.value.padEnd(8)} ${option.title}`),
|
|
69
|
+
` выбор пишется в \`variants\` конфига: {"${axis.name}": "${axis.options[0]?.value ?? ''}"}`,
|
|
70
|
+
]);
|
|
71
|
+
/**
|
|
72
|
+
* Брошенное: файл лежит, а ресурс за ним больше не берут. Печатается отдельным списком и с
|
|
73
|
+
* подсказкой — стереть его пакет не вправе, а промолчать не может: агент читает такой файл как
|
|
74
|
+
* действующее правило.
|
|
75
|
+
*/
|
|
76
|
+
const abandonedLines = (result) => result.abandoned.length
|
|
77
|
+
? [
|
|
78
|
+
`лежит от ресурсов, которые больше не берутся: ${result.abandoned.length}`,
|
|
79
|
+
...result.abandoned.map((path) => ` ${path}`),
|
|
80
|
+
' их не стирает никто: рядом может лежать написанное проектом — убирать вручную',
|
|
81
|
+
]
|
|
82
|
+
: [];
|
|
83
|
+
/**
|
|
84
|
+
* Ресурс, у которого нет вида под выбор дерева. Называется и то, чего не хватает, и оба способа
|
|
85
|
+
* это снять: отказ без действия обходят, а не исполняют.
|
|
86
|
+
*/
|
|
87
|
+
const gapLines = (result) => result.gaps.flatMap((gap) => [
|
|
88
|
+
` ${gap.kind}/${gap.name} — есть только под ${gap.axis}: ${gap.available.join(', ')}, а выбран «${gap.chosen}»`,
|
|
89
|
+
` либо заведи вид под «${gap.chosen}», либо назови в skip: ${gap.ids.join(', ')}`,
|
|
90
|
+
]);
|
|
91
|
+
/**
|
|
92
|
+
* Гарды, которых нет в настройке агента, и готовый кусок для неё.
|
|
93
|
+
*
|
|
94
|
+
* Кусок печатается целиком, а не одними именами: правку в свою настройку делает проект, и
|
|
95
|
+
* список «не хватает трёх» заставляет собирать JSON по памяти — там и теряется образец вызова.
|
|
96
|
+
*/
|
|
97
|
+
const unboundLines = (result) => result.unbound.length
|
|
98
|
+
? [
|
|
99
|
+
`гарды разложены, но в \`${SETTINGS_PATH}\` их не зовёт никто: ${result.unbound.length}`,
|
|
100
|
+
...result.unbound.map((binding) => ` ${binding.path} — ${binding.event}${binding.matcher ? ` ${binding.matcher}` : ''}`),
|
|
101
|
+
` вставь в \`${SETTINGS_PATH}\` раздел \`hooks\` — готовый кусок ниже:`,
|
|
102
|
+
...JSON.stringify({ hooks: hooksSection(result.unbound) }, null, 4)
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map((line) => ` ${line}`),
|
|
105
|
+
]
|
|
106
|
+
: [];
|
|
56
107
|
const describe = (result) => [
|
|
57
108
|
...holes(result),
|
|
109
|
+
...gapLines(result),
|
|
110
|
+
...unboundLines(result),
|
|
58
111
|
...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
|
|
59
112
|
...unfilled(result).map((entry) => ` ${entry.path} — ${COMPANION_WORD[entry.state]}`),
|
|
113
|
+
...abandonedLines(result),
|
|
60
114
|
];
|
|
115
|
+
/** Имена дырок во всех ресурсах, которые дерево берёт. Без конфига — ни одной: выбор неизвестен. */
|
|
116
|
+
function placeholdersIn(config, assetsDir) {
|
|
117
|
+
if (!config) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const names = [];
|
|
121
|
+
for (const asset of collectAssets(config, assetsDir)) {
|
|
122
|
+
for (const name of placeholdersOf(asset.text)) {
|
|
123
|
+
if (!names.includes(name)) {
|
|
124
|
+
names.push(name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return names.sort();
|
|
129
|
+
}
|
|
61
130
|
/**
|
|
62
131
|
* Что проект выбрал при заведении конфига. Пустой список — весь набор; выбор приходит сюда
|
|
63
132
|
* готовым, потому что взять его негде, кроме края: у строки запуска флаг, у терминала вопрос,
|
|
64
133
|
* а у неинтерактивного прогона нет ни того ни другого — и решать это команде не по чину.
|
|
65
134
|
*/
|
|
66
|
-
export function init(root, only = []) {
|
|
135
|
+
export function init(root, only = [], variants = {}, assetsDir = '') {
|
|
67
136
|
const path = join(root, CONFIG_PATH);
|
|
68
137
|
if (existsSync(path)) {
|
|
69
138
|
return { code: 0, lines: [`${CONFIG_PATH} уже есть — оставлен как есть`] };
|
|
70
139
|
}
|
|
71
|
-
const config = { vars: {}, layout: DEFAULT_LAYOUT, only, skip: [] };
|
|
140
|
+
const config = { vars: {}, layout: DEFAULT_LAYOUT, variants, only, skip: [] };
|
|
72
141
|
mkdirSync(join(root, OVERRIDES_DIR), { recursive: true });
|
|
73
142
|
mkdirSync(join(root, CONFIG_PATH, '..'), { recursive: true });
|
|
74
143
|
writeFileSync(path, `${JSON.stringify(config, null, 4)}\n`, 'utf8');
|
|
144
|
+
// Значения, которых пакет ждёт от дерева, называются здесь, а не отказом раскладки: узнать
|
|
145
|
+
// о них на первом же `sync` значит начать установку с ошибки, притом что список известен
|
|
146
|
+
// сразу — он вычитывается из текстов выбранных ресурсов.
|
|
147
|
+
const wanted = assetsDir ? placeholdersIn(readConfig(root), assetsDir) : [];
|
|
75
148
|
return {
|
|
76
149
|
code: 0,
|
|
77
150
|
lines: [
|
|
78
151
|
`заведён ${CONFIG_PATH}`,
|
|
79
152
|
only.length ? `выбрано ресурсов: ${only.length}` : 'выбрано всё, что везёт пакет',
|
|
153
|
+
...Object.entries(variants).map(([axis, value]) => `${axis}: ${value}`),
|
|
80
154
|
`заведён ${OVERRIDES_DIR}/ — надстройки проекта кладутся сюда путём ресурса`,
|
|
81
|
-
|
|
155
|
+
...(wanted.length
|
|
156
|
+
? [
|
|
157
|
+
`значения, которых ждут ресурсы, — впиши их в \`vars\` конфига: ${wanted.length}`,
|
|
158
|
+
...wanted.map((name) => ` {{${name}}}`),
|
|
159
|
+
]
|
|
160
|
+
: []),
|
|
161
|
+
'дальше: `agent-kit sync`, а своё дописывается в `.claude/rt-kit/gate-map.sh` и `project.sh`',
|
|
82
162
|
],
|
|
83
163
|
};
|
|
84
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Отказ на устаревшей сборке. Раскладка из неё положила бы прежнюю редакцию ресурса и назвала
|
|
167
|
+
* это сделанным: неправда дороже отказа — её замечают, когда правленое правило не действует.
|
|
168
|
+
*/
|
|
169
|
+
const staleRefusal = (stale) => ({
|
|
170
|
+
code: 1,
|
|
171
|
+
lines: [
|
|
172
|
+
'раскладка не начата: строка запуска читает собранное, а ресурсы правлены позже',
|
|
173
|
+
` правлено: ${stale.newest}`,
|
|
174
|
+
` исходные ресурсы: ${stale.source}`,
|
|
175
|
+
' собери пакет и повтори',
|
|
176
|
+
],
|
|
177
|
+
});
|
|
85
178
|
export function sync(env, check) {
|
|
86
179
|
const { root, version, assetsDir } = env;
|
|
87
180
|
const config = readConfig(root);
|
|
88
181
|
if (!config) {
|
|
89
182
|
return { code: 1, lines: [NO_CONFIG] };
|
|
90
183
|
}
|
|
184
|
+
if (env.stale) {
|
|
185
|
+
return staleRefusal(env.stale);
|
|
186
|
+
}
|
|
187
|
+
// Ось без ответа отбивает раскладку целиком, а не пропускает свои ресурсы молча: правило
|
|
188
|
+
// поставки бывает в трёх видах, и дерево, не назвавшее свой, осталось бы вовсе без правила
|
|
189
|
+
// поставки — заметить это можно было бы только по тому, что гейт перестал его требовать.
|
|
190
|
+
const unanswered = unansweredAxes(readAxes(assetsDir), config.variants);
|
|
191
|
+
if (unanswered.length) {
|
|
192
|
+
return { code: 1, lines: ['раскладка не начата: не выбран вид', ...axisLines(unanswered)] };
|
|
193
|
+
}
|
|
91
194
|
if (check) {
|
|
92
195
|
const result = planSync(config, root, version, assetsDir);
|
|
93
196
|
const pending = pendingOf(result);
|
|
94
197
|
// Незаполненный компаньон — такое же расхождение, как отставший файл: правило разложено,
|
|
95
198
|
// а имён этого дерева при нём нет, и агент читает указание, которому некуда примениться.
|
|
96
199
|
const empty = unfilled(result);
|
|
97
|
-
|
|
200
|
+
// Гард, которого не зовёт настройка агента, — такое же расхождение, как отставший файл:
|
|
201
|
+
// он разложен, он коммитится, и по дереву его не отличить от работающего.
|
|
202
|
+
const count = result.missing.size + result.gaps.length + pending.length + empty.length + result.unbound.length;
|
|
203
|
+
if (!count) {
|
|
98
204
|
return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`] };
|
|
99
205
|
}
|
|
100
|
-
return {
|
|
101
|
-
code: 1,
|
|
102
|
-
lines: [`sync --check: расхождений ${result.missing.size + pending.length + empty.length}`, ...describe(result)],
|
|
103
|
-
};
|
|
206
|
+
return { code: 1, lines: [`sync --check: расхождений ${count}`, ...describe(result)] };
|
|
104
207
|
}
|
|
105
208
|
const result = runSync(config, root, version, assetsDir);
|
|
106
209
|
// Перечисляются только дырки: раскладка не начата, и назвать файл «положенным» значило бы
|
|
@@ -108,6 +211,14 @@ export function sync(env, check) {
|
|
|
108
211
|
if (result.missing.size) {
|
|
109
212
|
return { code: 1, lines: ['раскладка не начата: нечего подставить в дырки', ...holes(result)] };
|
|
110
213
|
}
|
|
214
|
+
// Ресурс без вида под выбор дерева отбивает раскладку целиком, а не выпадает из неё молча:
|
|
215
|
+
// правило, разложенное без инструмента, который оно зовёт, читается как действующее.
|
|
216
|
+
if (result.gaps.length) {
|
|
217
|
+
return {
|
|
218
|
+
code: 1,
|
|
219
|
+
lines: ['раскладка не начата: у этих ресурсов нет вида под выбор дерева', ...gapLines(result)],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
111
222
|
const refused = result.planned.filter((entry) => isRefusal(entry.outcome));
|
|
112
223
|
if (refused.length) {
|
|
113
224
|
return {
|
|
@@ -116,14 +227,74 @@ export function sync(env, check) {
|
|
|
116
227
|
'раскладка не начата: эти файлы пакет переписывать не станет',
|
|
117
228
|
...refused.map((entry) => ` ${entry.path} — ${DONE_WORD[entry.outcome]}`),
|
|
118
229
|
'правку надо либо перенести в надстройку, либо снять — и повторить',
|
|
230
|
+
`положенное не пакетом отдаётся ему командой \`agent-kit adopt\`: прежнее содержимое ляжет рядом с пометкой \`${KEPT_SUFFIX}\``,
|
|
119
231
|
],
|
|
120
232
|
};
|
|
121
233
|
}
|
|
234
|
+
// Раскладка удалась, а гарды могут остаться неподключёнными: настройка агента принадлежит
|
|
235
|
+
// дереву, и пакет в неё не пишет. Код возврата остаётся нулевым — файлы легли, — но молчать
|
|
236
|
+
// об этом нельзя: узнают иначе, когда что-нибудь пройдёт мимо гарда.
|
|
122
237
|
return {
|
|
123
238
|
code: 0,
|
|
124
|
-
lines:
|
|
125
|
-
|
|
126
|
-
|
|
239
|
+
lines: [
|
|
240
|
+
...(result.written.length
|
|
241
|
+
? [`разложено файлов: ${result.written.length}`, ...result.written.map((path) => ` ${path}`)]
|
|
242
|
+
: ['всё уже разложено']),
|
|
243
|
+
...unboundLines(result),
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Отдать пакету файл, который лежит в дереве не от него.
|
|
249
|
+
*
|
|
250
|
+
* Пока команды не было, `sync` отказывал на такой файл поимённо и предлагал «перенести правку в
|
|
251
|
+
* надстройку или снять» — то есть работу, которую делают руками, и до неё же сводилась вся
|
|
252
|
+
* установка в живое дерево. При этом один чужой файл останавливал раскладку целиком.
|
|
253
|
+
*
|
|
254
|
+
* Молчаливой перезаписи здесь нет и быть не может: в файле лежит то, чего в дереве больше нигде
|
|
255
|
+
* нет. Поэтому переход — это переименование прежнего содержимого рядом и повторный `sync`:
|
|
256
|
+
* пакет кладёт своё, а прежнее остаётся на диске под именем с пометкой и ждёт разбора.
|
|
257
|
+
*/
|
|
258
|
+
export function adopt(env, names) {
|
|
259
|
+
const { root, version, assetsDir } = env;
|
|
260
|
+
const config = readConfig(root);
|
|
261
|
+
if (!config) {
|
|
262
|
+
return { code: 1, lines: [NO_CONFIG] };
|
|
263
|
+
}
|
|
264
|
+
const foreign = planSync(config, root, version, assetsDir).planned.filter((entry) => entry.outcome === 'foreign');
|
|
265
|
+
if (!foreign.length) {
|
|
266
|
+
return { code: 0, lines: ['чужих файлов на путях пакета нет'] };
|
|
267
|
+
}
|
|
268
|
+
// Имена принимаются как их печатает `sync`: путём в дереве. Без имён отдаются все — на
|
|
269
|
+
// свежей установке их бывает столько же, сколько ресурсов, и перечислять их руками значит
|
|
270
|
+
// делать ту же работу, ради которой команда заведена.
|
|
271
|
+
const wanted = names.length
|
|
272
|
+
? foreign.filter((entry) => names.some((name) => entry.path.endsWith(name)))
|
|
273
|
+
: foreign;
|
|
274
|
+
if (!wanted.length) {
|
|
275
|
+
return {
|
|
276
|
+
code: 1,
|
|
277
|
+
lines: ['ни одно имя не отвечает чужому файлу; чужие сейчас такие:', ...foreign.map((e) => ` ${e.path}`)],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const moved = [];
|
|
281
|
+
const blocked = [];
|
|
282
|
+
for (const entry of wanted) {
|
|
283
|
+
const path = join(root, entry.path);
|
|
284
|
+
const kept = `${path}${KEPT_SUFFIX}`;
|
|
285
|
+
if (existsSync(kept)) {
|
|
286
|
+
blocked.push(` ${entry.path}${KEPT_SUFFIX} — уже лежит, разбери его прежде`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
renameSync(path, kept);
|
|
290
|
+
moved.push(` ${entry.path} → ${entry.path}${KEPT_SUFFIX}`);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
code: blocked.length ? 1 : 0,
|
|
294
|
+
lines: [
|
|
295
|
+
...(moved.length ? [`прежнее содержимое отложено: ${moved.length}`, ...moved, 'дальше: `agent-kit sync`'] : []),
|
|
296
|
+
...(blocked.length ? ['не тронуто:', ...blocked] : []),
|
|
297
|
+
],
|
|
127
298
|
};
|
|
128
299
|
}
|
|
129
300
|
export function doctor(env) {
|
|
@@ -139,12 +310,26 @@ export function doctor(env) {
|
|
|
139
310
|
}
|
|
140
311
|
const catalog = readCatalog(assetsDir);
|
|
141
312
|
const taken = collectAssets(config, assetsDir).length;
|
|
313
|
+
const unanswered = unansweredAxes(readAxes(assetsDir), config.variants);
|
|
314
|
+
const chosen = Object.entries(config.variants).map(([axis, value]) => `${axis}: ${value}`);
|
|
315
|
+
// Ресурс чужого вида в «не выбрано» не идёт: проект от него не отказывался — его в этом
|
|
316
|
+
// дереве не существует, как не существует второго правила поставки.
|
|
317
|
+
const foreignVariant = catalog.filter((entry) => entry.variant !== null && config.variants[entry.variant.axis] !== entry.variant.value);
|
|
318
|
+
const other = foreignVariant.length;
|
|
319
|
+
// Ресурс, названный и в отказе, и чужим видом, считается один раз — иначе «не выбрано»
|
|
320
|
+
// уходит в минус, а минус в отчёте читается как поломка счёта, а не как двойной счёт.
|
|
321
|
+
const skipped = config.skip.filter((id) => foreignVariant.every((entry) => entry.id !== id)).length;
|
|
142
322
|
const lines = [
|
|
143
323
|
`пакет v${version}, везёт ресурсов ${catalog.length}, взято ${taken}`,
|
|
144
|
-
`не выбрано: ${catalog.length - taken -
|
|
324
|
+
`не выбрано: ${catalog.length - taken - skipped - other}, пропущено: ${skipped}, другой вид: ${other}`,
|
|
145
325
|
`значений в конфиге: ${Object.keys(config.vars).length}`,
|
|
326
|
+
...chosen,
|
|
146
327
|
...[...counted].map(([outcome, count]) => `${STATE_WORD[outcome]}: ${count}`),
|
|
147
328
|
...describe(result),
|
|
329
|
+
...(unanswered.length ? ['не выбран вид — раскладка не начнётся:', ...axisLines(unanswered)] : []),
|
|
330
|
+
// Про устаревшую сборку `doctor` говорит, но отказом её не считает: он ничего не пишет,
|
|
331
|
+
// и прочитать состояние дерева можно и из вчерашней сборки — знать бы, что она вчерашняя.
|
|
332
|
+
...(env.stale ? ['собранное отстало от исходников — раскладка откажет:', ` правлено: ${env.stale.newest}`] : []),
|
|
148
333
|
];
|
|
149
334
|
return { code: 0, lines };
|
|
150
335
|
}
|
|
@@ -168,10 +353,13 @@ export function list(env) {
|
|
|
168
353
|
if (!config) {
|
|
169
354
|
return 'везёт пакет';
|
|
170
355
|
}
|
|
356
|
+
if (entry.variant && config.variants[entry.variant.axis] !== entry.variant.value) {
|
|
357
|
+
return OTHER_VARIANT;
|
|
358
|
+
}
|
|
171
359
|
if (config.skip.includes(entry.id)) {
|
|
172
360
|
return SKIPPED;
|
|
173
361
|
}
|
|
174
|
-
if (!isChosen(entry, config
|
|
362
|
+
if (!isChosen(entry, config)) {
|
|
175
363
|
return NOT_CHOSEN;
|
|
176
364
|
}
|
|
177
365
|
const outcome = planned.get(entry.id);
|