@rt-tools/agent-kit 0.1.0 → 0.2.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 +29 -3
- package/assets/laws/access.md +3 -12
- package/assets/laws/admin-lists.md +9 -21
- package/assets/laws/admin-navigation.md +3 -15
- package/assets/laws/code-structure.md +5 -16
- package/assets/laws/delivery.md +2 -16
- package/assets/laws/entity-editing.md +7 -20
- package/assets/laws/entity-models.md +10 -17
- package/assets/laws/frontend-application.md +3 -14
- package/assets/laws/lib-imports.md +0 -13
- package/assets/laws/locales.md +2 -11
- package/assets/laws/project-documentation.md +7 -20
- package/assets/laws/reuse-first.md +0 -9
- package/assets/laws/search-visibility.md +0 -13
- package/assets/laws/shared-code.md +0 -13
- package/assets/laws/verifiability.md +0 -13
- package/bin/agent-kit.d.ts +1 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +63 -7
- package/bin/agent-kit.js.map +1 -1
- package/bin/prompt.d.ts +9 -0
- package/bin/prompt.d.ts.map +1 -0
- package/bin/prompt.js +57 -0
- package/bin/prompt.js.map +1 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/lib/assets.d.ts +2 -2
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +14 -29
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +44 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +86 -0
- package/lib/catalog.js.map +1 -0
- package/lib/commands.d.ts +13 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +85 -9
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +5 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +10 -5
- package/lib/config.js.map +1 -1
- package/lib/picker.d.ts +47 -0
- package/lib/picker.d.ts.map +1 -0
- package/lib/picker.js +112 -0
- package/lib/picker.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.2.0.tgz +0 -0
- package/rt-tools-agent-kit-0.1.0.tgz +0 -0
package/lib/commands.js
CHANGED
|
@@ -1,33 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Команды и их вывод. Печать отделена от работы: команда возвращает строки и код возврата,
|
|
3
3
|
* а `process.exit` зовёт только точка входа — иначе ни одну из них нельзя было бы проверить
|
|
4
4
|
* спекой, не перехватывая поток вывода.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { collectAssets } from './assets.js';
|
|
9
|
-
import {
|
|
9
|
+
import { isChosen, readCatalog } from './catalog.js';
|
|
10
|
+
import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, readConfig } from './config.js';
|
|
10
11
|
import { isRefusal } from './plan.js';
|
|
11
12
|
import { pendingOf, planSync, runSync } from './sync.js';
|
|
12
|
-
|
|
13
|
+
/** Что с файлом сделала раскладка. Прошедшее время здесь правда: `sync` уже записал. */
|
|
14
|
+
const DONE_WORD = {
|
|
13
15
|
create: 'положен',
|
|
14
16
|
update: 'переложен',
|
|
15
17
|
drift: 'правлен руками',
|
|
16
18
|
foreign: 'положен не пакетом',
|
|
17
19
|
ok: 'без изменений',
|
|
18
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Что с файлом **сейчас**. Отдельно от прошедшего времени потому, что `doctor` и `list` ничего
|
|
23
|
+
* не пишут: сказать «положен» о файле, которого в дереве нет, значит соврать читателю ровно там,
|
|
24
|
+
* где он пришёл узнать состояние.
|
|
25
|
+
*/
|
|
26
|
+
const STATE_WORD = {
|
|
27
|
+
create: 'нет в дереве',
|
|
28
|
+
update: 'отстал от пакета',
|
|
29
|
+
drift: 'правлен руками',
|
|
30
|
+
foreign: 'положен не пакетом',
|
|
31
|
+
ok: 'на месте',
|
|
32
|
+
};
|
|
33
|
+
const NOT_CHOSEN = 'не выбран';
|
|
34
|
+
const SKIPPED = 'пропущен';
|
|
35
|
+
const KIND_TITLE = {
|
|
36
|
+
laws: 'ЗАКОНЫ',
|
|
37
|
+
hooks: 'ХУКИ',
|
|
38
|
+
checks: 'ПРОВЕРКИ',
|
|
39
|
+
agents: 'АГЕНТЫ',
|
|
40
|
+
templates: 'ШАБЛОНЫ',
|
|
41
|
+
};
|
|
19
42
|
const NO_CONFIG = `нет \`${CONFIG_PATH}\` — начни с \`agent-kit init\``;
|
|
20
43
|
const holes = (result) => [...result.missing].map(([asset, names]) => ` ${asset}: нет значений для ${names.map((name) => `{{${name}}}`).join(', ')}`);
|
|
21
44
|
const describe = (result) => [
|
|
22
45
|
...holes(result),
|
|
23
|
-
...pendingOf(result).map((entry) => ` ${entry.path} — ${
|
|
46
|
+
...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
|
|
24
47
|
];
|
|
25
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Что проект выбрал при заведении конфига. Пустой список — весь набор; выбор приходит сюда
|
|
50
|
+
* готовым, потому что взять его негде, кроме края: у строки запуска флаг, у терминала вопрос,
|
|
51
|
+
* а у неинтерактивного прогона нет ни того ни другого — и решать это команде не по чину.
|
|
52
|
+
*/
|
|
53
|
+
export function init(root, only = []) {
|
|
26
54
|
const path = join(root, CONFIG_PATH);
|
|
27
55
|
if (existsSync(path)) {
|
|
28
56
|
return { code: 0, lines: [`${CONFIG_PATH} уже есть — оставлен как есть`] };
|
|
29
57
|
}
|
|
30
|
-
const config = { vars: {}, layout: DEFAULT_LAYOUT, skip: [] };
|
|
58
|
+
const config = { vars: {}, layout: DEFAULT_LAYOUT, only, skip: [] };
|
|
31
59
|
mkdirSync(join(root, OVERRIDES_DIR), { recursive: true });
|
|
32
60
|
mkdirSync(join(root, CONFIG_PATH, '..'), { recursive: true });
|
|
33
61
|
writeFileSync(path, `${JSON.stringify(config, null, 4)}\n`, 'utf8');
|
|
@@ -35,6 +63,7 @@ export function init(root) {
|
|
|
35
63
|
code: 0,
|
|
36
64
|
lines: [
|
|
37
65
|
`заведён ${CONFIG_PATH}`,
|
|
66
|
+
only.length ? `выбрано ресурсов: ${only.length}` : 'выбрано всё, что везёт пакет',
|
|
38
67
|
`заведён ${OVERRIDES_DIR}/ — надстройки проекта кладутся сюда путём ресурса`,
|
|
39
68
|
'дальше: заполни `vars` и запусти `agent-kit sync`',
|
|
40
69
|
],
|
|
@@ -66,7 +95,7 @@ export function sync(env, check) {
|
|
|
66
95
|
code: 1,
|
|
67
96
|
lines: [
|
|
68
97
|
'раскладка не начата: эти файлы пакет переписывать не станет',
|
|
69
|
-
...refused.map((entry) => ` ${entry.path} — ${
|
|
98
|
+
...refused.map((entry) => ` ${entry.path} — ${DONE_WORD[entry.outcome]}`),
|
|
70
99
|
'правку надо либо перенести в надстройку, либо снять — и повторить',
|
|
71
100
|
],
|
|
72
101
|
};
|
|
@@ -89,12 +118,59 @@ export function doctor(env) {
|
|
|
89
118
|
for (const entry of result.planned) {
|
|
90
119
|
counted.set(entry.outcome, (counted.get(entry.outcome) ?? 0) + 1);
|
|
91
120
|
}
|
|
121
|
+
const catalog = readCatalog(assetsDir);
|
|
122
|
+
const taken = collectAssets(config, assetsDir).length;
|
|
92
123
|
const lines = [
|
|
93
|
-
`пакет v${version}, ресурсов ${
|
|
124
|
+
`пакет v${version}, везёт ресурсов ${catalog.length}, взято ${taken}`,
|
|
125
|
+
`не выбрано: ${catalog.length - taken - config.skip.length}, пропущено: ${config.skip.length}`,
|
|
94
126
|
`значений в конфиге: ${Object.keys(config.vars).length}`,
|
|
95
|
-
...[...counted].map(([outcome, count]) => `${
|
|
127
|
+
...[...counted].map(([outcome, count]) => `${STATE_WORD[outcome]}: ${count}`),
|
|
96
128
|
...describe(result),
|
|
97
129
|
];
|
|
98
130
|
return { code: 0, lines };
|
|
99
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Что пакет везёт и что из этого взято здесь.
|
|
134
|
+
*
|
|
135
|
+
* Работает и без конфига: посмотреть, из чего выбирать, нужно раньше, чем выбор сделан, — иначе
|
|
136
|
+
* имена законов взять неоткуда, кроме исходников пакета.
|
|
137
|
+
*/
|
|
138
|
+
export function list(env) {
|
|
139
|
+
const { root, version, assetsDir } = env;
|
|
140
|
+
const config = readConfig(root);
|
|
141
|
+
const catalog = readCatalog(assetsDir);
|
|
142
|
+
const planned = new Map();
|
|
143
|
+
if (config) {
|
|
144
|
+
for (const entry of planSync(config, root, version, assetsDir).planned) {
|
|
145
|
+
planned.set(entry.asset, entry.outcome);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const stateOf = (entry) => {
|
|
149
|
+
if (!config) {
|
|
150
|
+
return 'везёт пакет';
|
|
151
|
+
}
|
|
152
|
+
if (config.skip.includes(entry.id)) {
|
|
153
|
+
return SKIPPED;
|
|
154
|
+
}
|
|
155
|
+
if (!isChosen(entry, config.only, config.skip)) {
|
|
156
|
+
return NOT_CHOSEN;
|
|
157
|
+
}
|
|
158
|
+
const outcome = planned.get(entry.id);
|
|
159
|
+
// Ресурса нет среди запланированных ровно в одном случае: в нём осталась дырка без
|
|
160
|
+
// значения, и до решения о судьбе файла раскладка не дошла.
|
|
161
|
+
return outcome ? STATE_WORD[outcome] : 'дырка без значения';
|
|
162
|
+
};
|
|
163
|
+
const name = catalog.reduce((found, entry) => Math.max(found, entry.name.length), 0);
|
|
164
|
+
const state = catalog.reduce((found, entry) => Math.max(found, stateOf(entry).length), 0);
|
|
165
|
+
const lines = [config ? `пакет v${version}` : `пакет v${version}, ${CONFIG_PATH} ещё не заведён`];
|
|
166
|
+
for (const kind of KINDS) {
|
|
167
|
+
const entries = catalog.filter((entry) => entry.kind === kind);
|
|
168
|
+
if (!entries.length) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
lines.push('', KIND_TITLE[kind]);
|
|
172
|
+
lines.push(...entries.map((entry) => ` ${entry.name.padEnd(name)} ${stateOf(entry).padEnd(state)} ${entry.title}`));
|
|
173
|
+
}
|
|
174
|
+
return { code: 0, lines };
|
|
175
|
+
}
|
|
100
176
|
//# sourceMappingURL=commands.js.map
|
package/lib/commands.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Три команды и их вывод. Печать отделена от работы: команда возвращает строки и код возврата,\n * а `process.exit` зовёт только точка входа — иначе ни одну из них нельзя было бы проверить\n * спекой, не перехватывая поток вывода.\n */\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { collectAssets } from './assets.js';\nimport { CONFIG_PATH, DEFAULT_LAYOUT, IConfig, OVERRIDES_DIR, readConfig } from './config.js';\nimport { IPlanned, isRefusal, TOutcome } from './plan.js';\nimport { ISyncResult, pendingOf, planSync, runSync } from './sync.js';\n\nexport interface IOutcomeOfCommand {\n readonly code: number;\n readonly lines: readonly string[];\n}\n\n/** Всё, что команда знает о мире: корень проекта, версия пакета и его каталог ресурсов. */\nexport interface IEnvironment {\n readonly root: string;\n readonly version: string;\n readonly assetsDir: string;\n}\n\nconst WORD: Readonly<Record<TOutcome, string>> = {\n create: 'положен',\n update: 'переложен',\n drift: 'правлен руками',\n foreign: 'положен не пакетом',\n ok: 'без изменений',\n};\n\nconst NO_CONFIG: string = `нет \\`${CONFIG_PATH}\\` — начни с \\`agent-kit init\\``;\n\nconst holes: (result: ISyncResult) => string[] = (result: ISyncResult): string[] =>\n [...result.missing].map(\n ([asset, names]: [string, readonly string[]]): string =>\n ` ${asset}: нет значений для ${names.map((name: string): string => `{{${name}}}`).join(', ')}`\n );\n\nconst describe: (result: ISyncResult) => string[] = (result: ISyncResult): string[] => [\n ...holes(result),\n ...pendingOf(result).map((entry: IPlanned): string => ` ${entry.path} — ${WORD[entry.outcome]}`),\n];\n\nexport function init(root: string): IOutcomeOfCommand {\n const path: string = join(root, CONFIG_PATH);\n if (existsSync(path)) {\n return { code: 0, lines: [`${CONFIG_PATH} уже есть — оставлен как есть`] };\n }\n\n const config: object = { vars: {}, layout: DEFAULT_LAYOUT, skip: [] };\n mkdirSync(join(root, OVERRIDES_DIR), { recursive: true });\n mkdirSync(join(root, CONFIG_PATH, '..'), { recursive: true });\n writeFileSync(path, `${JSON.stringify(config, null, 4)}\\n`, 'utf8');\n\n return {\n code: 0,\n lines: [\n `заведён ${CONFIG_PATH}`,\n `заведён ${OVERRIDES_DIR}/ — надстройки проекта кладутся сюда путём ресурса`,\n 'дальше: заполни `vars` и запусти `agent-kit sync`',\n ],\n };\n}\n\nexport function sync(env: IEnvironment, check: boolean): IOutcomeOfCommand {\n const { root, version, assetsDir } = env;\n const config: IConfig | null = readConfig(root);\n if (!config) {\n return { code: 1, lines: [NO_CONFIG] };\n }\n\n if (check) {\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n const pending: readonly IPlanned[] = pendingOf(result);\n if (!result.missing.size && !pending.length) {\n return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`] };\n }\n\n return { code: 1, lines: [`sync --check: расхождений ${result.missing.size + pending.length}`, ...describe(result)] };\n }\n\n const result: ISyncResult = runSync(config, root, version, assetsDir);\n // Перечисляются только дырки: раскладка не начата, и назвать файл «положенным» значило бы\n // сказать неправду — на диске его нет.\n if (result.missing.size) {\n return { code: 1, lines: ['раскладка не начата: нечего подставить в дырки', ...holes(result)] };\n }\n\n const refused: readonly IPlanned[] = result.planned.filter((entry: IPlanned): boolean => isRefusal(entry.outcome));\n if (refused.length) {\n return {\n code: 1,\n lines: [\n 'раскладка не начата: эти файлы пакет переписывать не станет',\n ...refused.map((entry: IPlanned): string => ` ${entry.path} — ${WORD[entry.outcome]}`),\n 'правку надо либо перенести в надстройку, либо снять — и повторить',\n ],\n };\n }\n\n return {\n code: 0,\n lines: result.written.length\n ? [`разложено файлов: ${result.written.length}`, ...result.written.map((path: string): string => ` ${path}`)]\n : ['всё уже разложено'],\n };\n}\n\nexport function doctor(env: IEnvironment): IOutcomeOfCommand {\n const { root, version, assetsDir } = env;\n const config: IConfig | null = readConfig(root);\n if (!config) {\n return { code: 1, lines: [NO_CONFIG] };\n }\n\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n const counted: Map<TOutcome, number> = new Map();\n for (const entry of result.planned) {\n counted.set(entry.outcome, (counted.get(entry.outcome) ?? 0) + 1);\n }\n\n const lines: string[] = [\n `пакет v${version}, ресурсов ${collectAssets(config, assetsDir).length}, из них пропущено ${config.skip.length}`,\n `значений в конфиге: ${Object.keys(config.vars).length}`,\n ...[...counted].map(([outcome, count]: [TOutcome, number]): string => `${WORD[outcome]}: ${count}`),\n ...describe(result),\n ];\n\n return { code: 0, lines };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Команды и их вывод. Печать отделена от работы: команда возвращает строки и код возврата,\n * а `process.exit` зовёт только точка входа — иначе ни одну из них нельзя было бы проверить\n * спекой, не перехватывая поток вывода.\n */\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { collectAssets } from './assets.js';\nimport { IEntryOfCatalog, isChosen, readCatalog } from './catalog.js';\nimport { CONFIG_PATH, DEFAULT_LAYOUT, IConfig, KINDS, OVERRIDES_DIR, readConfig, TKind } from './config.js';\nimport { IPlanned, isRefusal, TOutcome } from './plan.js';\nimport { ISyncResult, pendingOf, planSync, runSync } from './sync.js';\n\nexport interface IOutcomeOfCommand {\n readonly code: number;\n readonly lines: readonly string[];\n}\n\n/** Всё, что команда знает о мире: корень проекта, версия пакета и его каталог ресурсов. */\nexport interface IEnvironment {\n readonly root: string;\n readonly version: string;\n readonly assetsDir: string;\n}\n\n/** Что с файлом сделала раскладка. Прошедшее время здесь правда: `sync` уже записал. */\nconst DONE_WORD: Readonly<Record<TOutcome, string>> = {\n create: 'положен',\n update: 'переложен',\n drift: 'правлен руками',\n foreign: 'положен не пакетом',\n ok: 'без изменений',\n};\n\n/**\n * Что с файлом **сейчас**. Отдельно от прошедшего времени потому, что `doctor` и `list` ничего\n * не пишут: сказать «положен» о файле, которого в дереве нет, значит соврать читателю ровно там,\n * где он пришёл узнать состояние.\n */\nconst STATE_WORD: Readonly<Record<TOutcome, string>> = {\n create: 'нет в дереве',\n update: 'отстал от пакета',\n drift: 'правлен руками',\n foreign: 'положен не пакетом',\n ok: 'на месте',\n};\n\nconst NOT_CHOSEN: string = 'не выбран';\nconst SKIPPED: string = 'пропущен';\n\nconst KIND_TITLE: Readonly<Record<TKind, string>> = {\n laws: 'ЗАКОНЫ',\n hooks: 'ХУКИ',\n checks: 'ПРОВЕРКИ',\n agents: 'АГЕНТЫ',\n templates: 'ШАБЛОНЫ',\n};\n\nconst NO_CONFIG: string = `нет \\`${CONFIG_PATH}\\` — начни с \\`agent-kit init\\``;\n\nconst holes: (result: ISyncResult) => string[] = (result: ISyncResult): string[] =>\n [...result.missing].map(\n ([asset, names]: [string, readonly string[]]): string =>\n ` ${asset}: нет значений для ${names.map((name: string): string => `{{${name}}}`).join(', ')}`\n );\n\nconst describe: (result: ISyncResult) => string[] = (result: ISyncResult): string[] => [\n ...holes(result),\n ...pendingOf(result).map((entry: IPlanned): string => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),\n];\n\n/**\n * Что проект выбрал при заведении конфига. Пустой список — весь набор; выбор приходит сюда\n * готовым, потому что взять его негде, кроме края: у строки запуска флаг, у терминала вопрос,\n * а у неинтерактивного прогона нет ни того ни другого — и решать это команде не по чину.\n */\nexport function init(root: string, only: readonly string[] = []): IOutcomeOfCommand {\n const path: string = join(root, CONFIG_PATH);\n if (existsSync(path)) {\n return { code: 0, lines: [`${CONFIG_PATH} уже есть — оставлен как есть`] };\n }\n\n const config: object = { vars: {}, layout: DEFAULT_LAYOUT, only, skip: [] };\n mkdirSync(join(root, OVERRIDES_DIR), { recursive: true });\n mkdirSync(join(root, CONFIG_PATH, '..'), { recursive: true });\n writeFileSync(path, `${JSON.stringify(config, null, 4)}\\n`, 'utf8');\n\n return {\n code: 0,\n lines: [\n `заведён ${CONFIG_PATH}`,\n only.length ? `выбрано ресурсов: ${only.length}` : 'выбрано всё, что везёт пакет',\n `заведён ${OVERRIDES_DIR}/ — надстройки проекта кладутся сюда путём ресурса`,\n 'дальше: заполни `vars` и запусти `agent-kit sync`',\n ],\n };\n}\n\nexport function sync(env: IEnvironment, check: boolean): IOutcomeOfCommand {\n const { root, version, assetsDir } = env;\n const config: IConfig | null = readConfig(root);\n if (!config) {\n return { code: 1, lines: [NO_CONFIG] };\n }\n\n if (check) {\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n const pending: readonly IPlanned[] = pendingOf(result);\n if (!result.missing.size && !pending.length) {\n return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`] };\n }\n\n return { code: 1, lines: [`sync --check: расхождений ${result.missing.size + pending.length}`, ...describe(result)] };\n }\n\n const result: ISyncResult = runSync(config, root, version, assetsDir);\n // Перечисляются только дырки: раскладка не начата, и назвать файл «положенным» значило бы\n // сказать неправду — на диске его нет.\n if (result.missing.size) {\n return { code: 1, lines: ['раскладка не начата: нечего подставить в дырки', ...holes(result)] };\n }\n\n const refused: readonly IPlanned[] = result.planned.filter((entry: IPlanned): boolean => isRefusal(entry.outcome));\n if (refused.length) {\n return {\n code: 1,\n lines: [\n 'раскладка не начата: эти файлы пакет переписывать не станет',\n ...refused.map((entry: IPlanned): string => ` ${entry.path} — ${DONE_WORD[entry.outcome]}`),\n 'правку надо либо перенести в надстройку, либо снять — и повторить',\n ],\n };\n }\n\n return {\n code: 0,\n lines: result.written.length\n ? [`разложено файлов: ${result.written.length}`, ...result.written.map((path: string): string => ` ${path}`)]\n : ['всё уже разложено'],\n };\n}\n\nexport function doctor(env: IEnvironment): IOutcomeOfCommand {\n const { root, version, assetsDir } = env;\n const config: IConfig | null = readConfig(root);\n if (!config) {\n return { code: 1, lines: [NO_CONFIG] };\n }\n\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n const counted: Map<TOutcome, number> = new Map();\n for (const entry of result.planned) {\n counted.set(entry.outcome, (counted.get(entry.outcome) ?? 0) + 1);\n }\n\n const catalog: readonly IEntryOfCatalog[] = readCatalog(assetsDir);\n const taken: number = collectAssets(config, assetsDir).length;\n\n const lines: string[] = [\n `пакет v${version}, везёт ресурсов ${catalog.length}, взято ${taken}`,\n `не выбрано: ${catalog.length - taken - config.skip.length}, пропущено: ${config.skip.length}`,\n `значений в конфиге: ${Object.keys(config.vars).length}`,\n ...[...counted].map(([outcome, count]: [TOutcome, number]): string => `${STATE_WORD[outcome]}: ${count}`),\n ...describe(result),\n ];\n\n return { code: 0, lines };\n}\n\n/**\n * Что пакет везёт и что из этого взято здесь.\n *\n * Работает и без конфига: посмотреть, из чего выбирать, нужно раньше, чем выбор сделан, — иначе\n * имена законов взять неоткуда, кроме исходников пакета.\n */\nexport function list(env: IEnvironment): IOutcomeOfCommand {\n const { root, version, assetsDir } = env;\n const config: IConfig | null = readConfig(root);\n const catalog: readonly IEntryOfCatalog[] = readCatalog(assetsDir);\n const planned: Map<string, TOutcome> = new Map();\n\n if (config) {\n for (const entry of planSync(config, root, version, assetsDir).planned) {\n planned.set(entry.asset, entry.outcome);\n }\n }\n\n const stateOf: (entry: IEntryOfCatalog) => string = (entry: IEntryOfCatalog): string => {\n if (!config) {\n return 'везёт пакет';\n }\n if (config.skip.includes(entry.id)) {\n return SKIPPED;\n }\n if (!isChosen(entry, config.only, config.skip)) {\n return NOT_CHOSEN;\n }\n const outcome: TOutcome | undefined = planned.get(entry.id);\n\n // Ресурса нет среди запланированных ровно в одном случае: в нём осталась дырка без\n // значения, и до решения о судьбе файла раскладка не дошла.\n return outcome ? STATE_WORD[outcome] : 'дырка без значения';\n };\n\n const name: number = catalog.reduce((found: number, entry: IEntryOfCatalog): number => Math.max(found, entry.name.length), 0);\n const state: number = catalog.reduce((found: number, entry: IEntryOfCatalog): number => Math.max(found, stateOf(entry).length), 0);\n const lines: string[] = [config ? `пакет v${version}` : `пакет v${version}, ${CONFIG_PATH} ещё не заведён`];\n\n for (const kind of KINDS) {\n const entries: readonly IEntryOfCatalog[] = catalog.filter((entry: IEntryOfCatalog): boolean => entry.kind === kind);\n if (!entries.length) {\n continue;\n }\n lines.push('', KIND_TITLE[kind]);\n lines.push(\n ...entries.map(\n (entry: IEntryOfCatalog): string => ` ${entry.name.padEnd(name)} ${stateOf(entry).padEnd(state)} ${entry.title}`\n )\n );\n }\n\n return { code: 0, lines };\n}\n"]}
|
package/lib/config.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface IConfig {
|
|
|
6
6
|
readonly vars: Readonly<Record<string, string>>;
|
|
7
7
|
/** Куда класть каждый род ресурса, путями от корня проекта. */
|
|
8
8
|
readonly layout: Readonly<Record<TKind, string>>;
|
|
9
|
+
/**
|
|
10
|
+
* Ресурсы, которые проект выбрал, идентификаторами: `laws/access.md`. Пусто — берётся всё.
|
|
11
|
+
* Ограничивает только те роды, которые сам называет; как именно — `isChosen`.
|
|
12
|
+
*/
|
|
13
|
+
readonly only: readonly string[];
|
|
9
14
|
/** Ресурсы, от которых проект отказался, идентификаторами: `laws/money.md`. */
|
|
10
15
|
readonly skip: readonly string[];
|
|
11
16
|
}
|
package/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/config.ts"],"names":[],"mappings":"AAUA,2EAA2E;AAC3E,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEzE,eAAO,MAAM,KAAK,EAAE,SAAS,KAAK,EAAuD,CAAC;AAE1F,MAAM,WAAW,OAAO;IACpB,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,eAAO,MAAM,WAAW,EAAE,MAA8B,CAAC;AACzD,eAAO,MAAM,aAAa,EAAE,MAAmC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAM1D,CAAC;AAEF,qBAAa,WAAY,SAAQ,KAAK;CAAG;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/config.ts"],"names":[],"mappings":"AAUA,2EAA2E;AAC3E,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEzE,eAAO,MAAM,KAAK,EAAE,SAAS,KAAK,EAAuD,CAAC;AAE1F,MAAM,WAAW,OAAO;IACpB,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,eAAO,MAAM,WAAW,EAAE,MAA8B,CAAC;AACzD,eAAO,MAAM,aAAa,EAAE,MAAmC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAM1D,CAAC;AAEF,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAgCzC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA6BjD;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAUvD"}
|
package/lib/config.js
CHANGED
|
@@ -41,6 +41,13 @@ function stringMap(value, where) {
|
|
|
41
41
|
}
|
|
42
42
|
return result;
|
|
43
43
|
}
|
|
44
|
+
function idList(value, where) {
|
|
45
|
+
const list = value ?? [];
|
|
46
|
+
if (!Array.isArray(list) || list.some((entry) => typeof entry !== 'string')) {
|
|
47
|
+
throw new ConfigError(`${where}: ожидается список идентификаторов ресурсов`);
|
|
48
|
+
}
|
|
49
|
+
return list;
|
|
50
|
+
}
|
|
44
51
|
export function parseConfig(text) {
|
|
45
52
|
let raw;
|
|
46
53
|
try {
|
|
@@ -58,17 +65,15 @@ export function parseConfig(text) {
|
|
|
58
65
|
throw new ConfigError(`layout.${kind}: такого рода ресурсов нет — есть ${KINDS.join(', ')}`);
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
throw new ConfigError('skip: ожидается список идентификаторов ресурсов');
|
|
64
|
-
}
|
|
68
|
+
const only = idList(raw['only'], 'only');
|
|
69
|
+
const skip = idList(raw['skip'], 'skip');
|
|
65
70
|
// Пути раскладки — тоже значения проекта, и пакет их уже знает. Требовать их вторым списком
|
|
66
71
|
// значило бы держать одно и то же в двух местах: расходятся такие пары молча.
|
|
67
72
|
const derived = {};
|
|
68
73
|
for (const kind of KINDS) {
|
|
69
74
|
derived[`${kind.replace(/s$/, '')}sDir`] = layout[kind];
|
|
70
75
|
}
|
|
71
|
-
return { vars: { ...derived, ...stringMap(raw['vars'], 'vars') }, layout: layout,
|
|
76
|
+
return { vars: { ...derived, ...stringMap(raw['vars'], 'vars') }, layout: layout, only, skip };
|
|
72
77
|
}
|
|
73
78
|
/** Конфиг проекта; его отсутствие — не отказ, а повод сказать про `agent-kit init`. */
|
|
74
79
|
export function readConfig(root) {
|
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":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAKjC,MAAM,CAAC,MAAM,KAAK,GAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;
|
|
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 = 'laws' | 'hooks' | 'checks' | 'agents' | 'templates';\n\nexport const KINDS: readonly TKind[] = ['laws', 'hooks', 'checks', 'agents', 'templates'];\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/money.md`. */\n readonly skip: readonly string[];\n}\n\nexport const CONFIG_PATH: string = '.claude/rt-kit.json';\nexport const OVERRIDES_DIR: string = '.claude/rt-kit/overrides';\n\n/**\n * Умолчания раскладки. Это не единственно возможные пути, но менять их без нужды не стоит:\n * агент ищет законы в `docs/constitution`, потому что так написано в правилах, которые пакет\n * же и везёт.\n */\nexport const DEFAULT_LAYOUT: Readonly<Record<TKind, string>> = {\n laws: 'docs/constitution',\n hooks: '.claude/hooks',\n checks: 'tools',\n agents: '.claude/agents',\n templates: '.claude/rt-kit/templates',\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\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 { vars: { ...derived, ...stringMap(raw['vars'], 'vars') }, layout: layout as Record<TKind, string>, only, skip };\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/picker.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Выбиралка галочками — её состояние и разметка, без единого касания терминала.
|
|
3
|
+
*
|
|
4
|
+
* Отделено от ввода-вывода по той же причине, что и команды: нажатие клавиши — это переход
|
|
5
|
+
* состояния, и проверять его надо спекой, а не глазами на живом терминале. Сырой режим,
|
|
6
|
+
* перерисовка и восстановление курсора живут на краю, в `bin/prompt.ts`, и не решают ничего.
|
|
7
|
+
*/
|
|
8
|
+
export interface IChoice {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** Имя, которым ресурс называют в строке запуска: `access`. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** Заголовок ресурса — им человек и выбирает, имена ему ни о чём не говорят. */
|
|
13
|
+
readonly title: string;
|
|
14
|
+
}
|
|
15
|
+
export interface IPickerState {
|
|
16
|
+
readonly choices: readonly IChoice[];
|
|
17
|
+
readonly cursor: number;
|
|
18
|
+
readonly chosen: ReadonlySet<string>;
|
|
19
|
+
/** Выбор окончен, `chosen` — ответ. */
|
|
20
|
+
readonly done: boolean;
|
|
21
|
+
/** Выбор брошен: ответа нет, и подставлять вместо него умолчание нельзя. */
|
|
22
|
+
readonly cancelled: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Первым предложением взято всё: отказ от закона — решение, и принимать его должен человек. */
|
|
25
|
+
export declare function pickerOf(choices: readonly IChoice[]): IPickerState;
|
|
26
|
+
export declare const KEY_UP: string;
|
|
27
|
+
export declare const KEY_DOWN: string;
|
|
28
|
+
export declare const KEY_ESCAPE: string;
|
|
29
|
+
export declare const KEY_CANCEL: string;
|
|
30
|
+
/**
|
|
31
|
+
* Кусок сырого потока, разобранный на клавиши.
|
|
32
|
+
*
|
|
33
|
+
* Терминал не обещает по куску на нажатие: набранное быстро, вставленное из буфера и всё, что
|
|
34
|
+
* пришло по конвейеру, приезжает одной строкой. Неразобранный кусок читался бы как одна
|
|
35
|
+
* незнакомая клавиша, и выбиралка молча не отзывалась бы ни на что — так она и повисла на
|
|
36
|
+
* первом прогоне через конвейер.
|
|
37
|
+
*
|
|
38
|
+
* Escape-последовательность берётся целиком: `ESC [ A` — это стрелка, а не escape и две буквы.
|
|
39
|
+
*/
|
|
40
|
+
export declare function keysOf(chunk: string): readonly string[];
|
|
41
|
+
/** Переход состояния по одной нажатой клавише. Незнакомая клавиша не делает ничего. */
|
|
42
|
+
export declare function pressKey(state: IPickerState, key: string): IPickerState;
|
|
43
|
+
/** Кусок потока целиком: разобрать на клавиши и провести состояние через каждую. */
|
|
44
|
+
export declare const pressChunk: (state: IPickerState, chunk: string) => IPickerState;
|
|
45
|
+
/** Разметка выбиралки строками. Курсор — стрелкой, выбранное — крестиком в скобках. */
|
|
46
|
+
export declare function renderPicker(state: IPickerState, question: string): readonly string[];
|
|
47
|
+
//# sourceMappingURL=picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/picker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,OAAO;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC/B;AAED,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,YAAY,CAElE;AAUD,eAAO,MAAM,MAAM,EAAE,MAAsB,CAAC;AAC5C,eAAO,MAAM,QAAQ,EAAE,MAAsB,CAAC;AAC9C,eAAO,MAAM,UAAU,EAAE,MAAe,CAAC;AACzC,eAAO,MAAM,UAAU,EAAE,MAA+B,CAAC;AAiCzD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAcvD;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAwBvE;AAED,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,YACxB,CAAC;AAK1C,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAarF"}
|
package/lib/picker.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Выбиралка галочками — её состояние и разметка, без единого касания терминала.
|
|
3
|
+
*
|
|
4
|
+
* Отделено от ввода-вывода по той же причине, что и команды: нажатие клавиши — это переход
|
|
5
|
+
* состояния, и проверять его надо спекой, а не глазами на живом терминале. Сырой режим,
|
|
6
|
+
* перерисовка и восстановление курсора живут на краю, в `bin/prompt.ts`, и не решают ничего.
|
|
7
|
+
*/
|
|
8
|
+
/** Первым предложением взято всё: отказ от закона — решение, и принимать его должен человек. */
|
|
9
|
+
export function pickerOf(choices) {
|
|
10
|
+
return { choices, cursor: 0, chosen: new Set(choices.map((choice) => choice.id)), done: false, cancelled: false };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Клавиши сырого потока. Записаны кодами, а не знаками: управляющий знак посреди исходника
|
|
14
|
+
* невидим, и правка рядом с ним ломает разбор молча.
|
|
15
|
+
*
|
|
16
|
+
* Стрелка приходит целиком, тремя знаками, — сравнивать надо всю последовательность.
|
|
17
|
+
*/
|
|
18
|
+
const ESCAPE = String.fromCharCode(27);
|
|
19
|
+
export const KEY_UP = `${ESCAPE}[A`;
|
|
20
|
+
export const KEY_DOWN = `${ESCAPE}[B`;
|
|
21
|
+
export const KEY_ESCAPE = ESCAPE;
|
|
22
|
+
export const KEY_CANCEL = String.fromCharCode(3);
|
|
23
|
+
const UP = [KEY_UP, 'k'];
|
|
24
|
+
const DOWN = [KEY_DOWN, 'j'];
|
|
25
|
+
const DONE = ['\r', '\n'];
|
|
26
|
+
/** Ctrl-C и escape. Оба — «я передумал», и оба обязаны уйти без записи. */
|
|
27
|
+
const CANCEL = [KEY_CANCEL, KEY_ESCAPE];
|
|
28
|
+
function moved(state, step) {
|
|
29
|
+
const count = state.choices.length;
|
|
30
|
+
return count ? { ...state, cursor: (state.cursor + step + count) % count } : state;
|
|
31
|
+
}
|
|
32
|
+
function toggled(state) {
|
|
33
|
+
const choice = state.choices[state.cursor];
|
|
34
|
+
if (!choice) {
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
const chosen = new Set(state.chosen);
|
|
38
|
+
if (!chosen.delete(choice.id)) {
|
|
39
|
+
chosen.add(choice.id);
|
|
40
|
+
}
|
|
41
|
+
return { ...state, chosen };
|
|
42
|
+
}
|
|
43
|
+
function switched(state) {
|
|
44
|
+
const all = state.chosen.size === state.choices.length;
|
|
45
|
+
return { ...state, chosen: all ? new Set() : new Set(state.choices.map((choice) => choice.id)) };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Кусок сырого потока, разобранный на клавиши.
|
|
49
|
+
*
|
|
50
|
+
* Терминал не обещает по куску на нажатие: набранное быстро, вставленное из буфера и всё, что
|
|
51
|
+
* пришло по конвейеру, приезжает одной строкой. Неразобранный кусок читался бы как одна
|
|
52
|
+
* незнакомая клавиша, и выбиралка молча не отзывалась бы ни на что — так она и повисла на
|
|
53
|
+
* первом прогоне через конвейер.
|
|
54
|
+
*
|
|
55
|
+
* Escape-последовательность берётся целиком: `ESC [ A` — это стрелка, а не escape и две буквы.
|
|
56
|
+
*/
|
|
57
|
+
export function keysOf(chunk) {
|
|
58
|
+
const points = Array.from(chunk);
|
|
59
|
+
const keys = [];
|
|
60
|
+
for (let index = 0; index < points.length; index++) {
|
|
61
|
+
if (points[index] === ESCAPE && points[index + 1] === '[' && points[index + 2] !== undefined) {
|
|
62
|
+
keys.push(points.slice(index, index + 3).join(''));
|
|
63
|
+
index += 2;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
keys.push(points[index]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return keys;
|
|
70
|
+
}
|
|
71
|
+
/** Переход состояния по одной нажатой клавише. Незнакомая клавиша не делает ничего. */
|
|
72
|
+
export function pressKey(state, key) {
|
|
73
|
+
if (state.done || state.cancelled) {
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
if (UP.includes(key)) {
|
|
77
|
+
return moved(state, -1);
|
|
78
|
+
}
|
|
79
|
+
if (DOWN.includes(key)) {
|
|
80
|
+
return moved(state, 1);
|
|
81
|
+
}
|
|
82
|
+
if (CANCEL.includes(key)) {
|
|
83
|
+
return { ...state, cancelled: true };
|
|
84
|
+
}
|
|
85
|
+
if (DONE.includes(key)) {
|
|
86
|
+
return { ...state, done: true };
|
|
87
|
+
}
|
|
88
|
+
if (key === ' ') {
|
|
89
|
+
return toggled(state);
|
|
90
|
+
}
|
|
91
|
+
if (key === 'a') {
|
|
92
|
+
return switched(state);
|
|
93
|
+
}
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
/** Кусок потока целиком: разобрать на клавиши и провести состояние через каждую. */
|
|
97
|
+
export const pressChunk = (state, chunk) => keysOf(chunk).reduce(pressKey, state);
|
|
98
|
+
const widest = (choices) => choices.reduce((width, choice) => Math.max(width, choice.name.length), 0);
|
|
99
|
+
/** Разметка выбиралки строками. Курсор — стрелкой, выбранное — крестиком в скобках. */
|
|
100
|
+
export function renderPicker(state, question) {
|
|
101
|
+
const width = widest(state.choices);
|
|
102
|
+
return [
|
|
103
|
+
question,
|
|
104
|
+
' ↑↓ — двигаться, пробел — выбрать, a — все или ни одного, enter — готово',
|
|
105
|
+
'',
|
|
106
|
+
...state.choices.map((choice, index) => {
|
|
107
|
+
const mark = state.chosen.has(choice.id) ? 'x' : ' ';
|
|
108
|
+
return `${index === state.cursor ? '❯' : ' '} [${mark}] ${choice.name.padEnd(width)} ${choice.title}`;
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"picker.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/picker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Выбиралка галочками — её состояние и разметка, без единого касания терминала.\n *\n * Отделено от ввода-вывода по той же причине, что и команды: нажатие клавиши — это переход\n * состояния, и проверять его надо спекой, а не глазами на живом терминале. Сырой режим,\n * перерисовка и восстановление курсора живут на краю, в `bin/prompt.ts`, и не решают ничего.\n */\n\nexport interface IChoice {\n readonly id: string;\n /** Имя, которым ресурс называют в строке запуска: `access`. */\n readonly name: string;\n /** Заголовок ресурса — им человек и выбирает, имена ему ни о чём не говорят. */\n readonly title: string;\n}\n\nexport interface IPickerState {\n readonly choices: readonly IChoice[];\n readonly cursor: number;\n readonly chosen: ReadonlySet<string>;\n /** Выбор окончен, `chosen` — ответ. */\n readonly done: boolean;\n /** Выбор брошен: ответа нет, и подставлять вместо него умолчание нельзя. */\n readonly cancelled: boolean;\n}\n\n/** Первым предложением взято всё: отказ от закона — решение, и принимать его должен человек. */\nexport function pickerOf(choices: readonly IChoice[]): IPickerState {\n return { choices, cursor: 0, chosen: new Set(choices.map((choice: IChoice): string => choice.id)), done: false, cancelled: false };\n}\n\n/**\n * Клавиши сырого потока. Записаны кодами, а не знаками: управляющий знак посреди исходника\n * невидим, и правка рядом с ним ломает разбор молча.\n *\n * Стрелка приходит целиком, тремя знаками, — сравнивать надо всю последовательность.\n */\nconst ESCAPE: string = String.fromCharCode(27);\n\nexport const KEY_UP: string = `${ESCAPE}[A`;\nexport const KEY_DOWN: string = `${ESCAPE}[B`;\nexport const KEY_ESCAPE: string = ESCAPE;\nexport const KEY_CANCEL: string = String.fromCharCode(3);\n\nconst UP: readonly string[] = [KEY_UP, 'k'];\nconst DOWN: readonly string[] = [KEY_DOWN, 'j'];\nconst DONE: readonly string[] = ['\\r', '\\n'];\n/** Ctrl-C и escape. Оба — «я передумал», и оба обязаны уйти без записи. */\nconst CANCEL: readonly string[] = [KEY_CANCEL, KEY_ESCAPE];\n\nfunction moved(state: IPickerState, step: number): IPickerState {\n const count: number = state.choices.length;\n\n return count ? { ...state, cursor: (state.cursor + step + count) % count } : state;\n}\n\nfunction toggled(state: IPickerState): IPickerState {\n const choice: IChoice | undefined = state.choices[state.cursor];\n if (!choice) {\n return state;\n }\n const chosen: Set<string> = new Set(state.chosen);\n if (!chosen.delete(choice.id)) {\n chosen.add(choice.id);\n }\n\n return { ...state, chosen };\n}\n\nfunction switched(state: IPickerState): IPickerState {\n const all: boolean = state.chosen.size === state.choices.length;\n\n return { ...state, chosen: all ? new Set() : new Set(state.choices.map((choice: IChoice): string => choice.id)) };\n}\n\n/**\n * Кусок сырого потока, разобранный на клавиши.\n *\n * Терминал не обещает по куску на нажатие: набранное быстро, вставленное из буфера и всё, что\n * пришло по конвейеру, приезжает одной строкой. Неразобранный кусок читался бы как одна\n * незнакомая клавиша, и выбиралка молча не отзывалась бы ни на что — так она и повисла на\n * первом прогоне через конвейер.\n *\n * Escape-последовательность берётся целиком: `ESC [ A` — это стрелка, а не escape и две буквы.\n */\nexport function keysOf(chunk: string): readonly string[] {\n const points: readonly string[] = Array.from(chunk);\n const keys: string[] = [];\n\n for (let index: number = 0; index < points.length; index++) {\n if (points[index] === ESCAPE && points[index + 1] === '[' && points[index + 2] !== undefined) {\n keys.push(points.slice(index, index + 3).join(''));\n index += 2;\n } else {\n keys.push(points[index]);\n }\n }\n\n return keys;\n}\n\n/** Переход состояния по одной нажатой клавише. Незнакомая клавиша не делает ничего. */\nexport function pressKey(state: IPickerState, key: string): IPickerState {\n if (state.done || state.cancelled) {\n return state;\n }\n if (UP.includes(key)) {\n return moved(state, -1);\n }\n if (DOWN.includes(key)) {\n return moved(state, 1);\n }\n if (CANCEL.includes(key)) {\n return { ...state, cancelled: true };\n }\n if (DONE.includes(key)) {\n return { ...state, done: true };\n }\n if (key === ' ') {\n return toggled(state);\n }\n if (key === 'a') {\n return switched(state);\n }\n\n return state;\n}\n\n/** Кусок потока целиком: разобрать на клавиши и провести состояние через каждую. */\nexport const pressChunk: (state: IPickerState, chunk: string) => IPickerState = (state: IPickerState, chunk: string): IPickerState =>\n keysOf(chunk).reduce(pressKey, state);\n\nconst widest: (choices: readonly IChoice[]) => number = (choices: readonly IChoice[]): number =>\n choices.reduce((width: number, choice: IChoice): number => Math.max(width, choice.name.length), 0);\n\n/** Разметка выбиралки строками. Курсор — стрелкой, выбранное — крестиком в скобках. */\nexport function renderPicker(state: IPickerState, question: string): readonly string[] {\n const width: number = widest(state.choices);\n\n return [\n question,\n ' ↑↓ — двигаться, пробел — выбрать, a — все или ни одного, enter — готово',\n '',\n ...state.choices.map((choice: IChoice, index: number): string => {\n const mark: string = state.chosen.has(choice.id) ? 'x' : ' ';\n\n return `${index === state.cursor ? '❯' : ' '} [${mark}] ${choice.name.padEnd(width)} ${choice.title}`;\n }),\n ];\n}\n"]}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|