agent-quality-kit 0.2.5 → 0.4.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 +72 -0
- package/README.ru.md +71 -0
- package/kit/gates/_native.sh +35 -0
- package/kit/gates/_skip.sh +34 -3
- package/kit/gates/commit-explains-itself/gate.yml +1 -0
- package/kit/gates/complexity-limit/gate.yml +1 -0
- package/kit/gates/dead-code/gate.yml +1 -0
- package/kit/gates/deps-are-pinned/gate.yml +1 -0
- package/kit/gates/duplicate-code/gate.yml +1 -0
- package/kit/gates/entry-links-exist/gate.yml +1 -0
- package/kit/gates/file-size-limit/gate.yml +1 -0
- package/kit/gates/gate-has-samples/gate.yml +1 -0
- package/kit/gates/gates-are-runnable/gate.yml +1 -0
- package/kit/gates/gates-run-in-ci/gate.yml +1 -0
- package/kit/gates/lesson-has-outcome/gate.yml +1 -0
- package/kit/gates/no-print-in-prod/check.sh +3 -1
- package/kit/gates/no-print-in-prod/gate.yml +1 -0
- package/kit/gates/secrets-not-in-code/gate.yml +1 -0
- package/kit/gates/swallowed-error/gate.yml +1 -0
- package/kit/gates/todo-without-task/gate.yml +1 -0
- package/package.json +1 -1
- package/tool/commands/badge.mjs +79 -0
- package/tool/commands/doctor.mjs +38 -41
- package/tool/commands/gates.mjs +121 -112
- package/tool/commands/project.mjs +84 -85
- package/tool/commands/report.mjs +194 -0
- package/tool/i18n/en.mjs +436 -0
- package/tool/i18n/index.mjs +33 -0
- package/tool/i18n/ru.mjs +437 -0
- package/tool/i18n/templates-en.mjs +164 -0
- package/tool/i18n/templates-ru.mjs +170 -0
- package/tool/lib/core.mjs +5 -1
- package/tool/lib/manifest.mjs +12 -31
- package/tool/lib/repo.mjs +45 -21
- package/tool/lib/templates.mjs +38 -182
- package/tool/program.mjs +36 -9
- package/tool/selfcheck/gates.sh +7 -1
- package/tool/selfcheck/smoke.sh +169 -0
- package/tool/selfcheck/units.mjs +101 -5
package/tool/commands/gates.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
detectFacts, readCatalog, pickRecipe, triggerVerdict, stems, overlap, matchCatalog,
|
|
14
14
|
} from "../lib/repo.mjs";
|
|
15
15
|
import { GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE } from "../lib/templates.mjs";
|
|
16
|
+
import { L } from "../i18n/index.mjs";
|
|
16
17
|
|
|
17
18
|
// Ставит гейт из каталога в проект. Проверка КОПИРУЕТСЯ в репозиторий, а не остаётся
|
|
18
19
|
// ссылкой в пакет: при установке через npx пакет временный, и завтра команда в манифесте
|
|
@@ -24,7 +25,7 @@ import { GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE } from "../lib/te
|
|
|
24
25
|
// читает окружение и выдаёт тысячу чужих нарушений.
|
|
25
26
|
async function installGate(slug, man, facts) {
|
|
26
27
|
const src = join(GATES_SRC, slug);
|
|
27
|
-
if (!(await exists(src))) die(
|
|
28
|
+
if (!(await exists(src))) die(L.add.noSuchGate(slug, `${SELF} doctor`));
|
|
28
29
|
|
|
29
30
|
const rec = { slug, ...parseManifest(await readFile(join(src, "gate.yml"), "utf8")) };
|
|
30
31
|
const dst = join(CWD, PROJECT_GATES, slug);
|
|
@@ -33,14 +34,29 @@ async function installGate(slug, man, facts) {
|
|
|
33
34
|
|
|
34
35
|
// Общий список исключений едет вместе с проверкой: без него она читает окружение и
|
|
35
36
|
// зависимости, и человек получает тысячу чужих нарушений вместо сотни своих.
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
for (const helper of ["_skip.sh", "_native.sh"]) {
|
|
38
|
+
const from = join(GATES_SRC, helper);
|
|
39
|
+
if (await exists(from)) await copyFile(from, join(CWD, PROJECT_GATES, helper));
|
|
40
|
+
}
|
|
38
41
|
|
|
39
42
|
// Команда под стек проекта, с путями внутри репозитория, а не внутри пакета.
|
|
40
|
-
const
|
|
43
|
+
const picked = String(pickRecipe(rec, facts) || "");
|
|
44
|
+
let cmd = picked
|
|
41
45
|
.replace(/\{gate\}/g, `${PROJECT_GATES}/${slug}`)
|
|
42
46
|
.replace(/\{dir\}/g, ".");
|
|
43
|
-
if (!cmd) die(
|
|
47
|
+
if (!cmd) die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack));
|
|
48
|
+
|
|
49
|
+
// Родной инструмент не знает про наши образцы и выдаёт их как находки — в любом проекте,
|
|
50
|
+
// куда поставили гейты. Заворачиваем его в общий фильтр. Переносимая проверка фильтрует
|
|
51
|
+
// себя сама, её заворачивать незачем.
|
|
52
|
+
//
|
|
53
|
+
// Обёртка ставится ЗДЕСЬ, а не в самом рецепте, ровно по одной причине: приёмка каталога
|
|
54
|
+
// (`gates.sh`) гоняет рецепт по красному образцу напрямую, без обёртки, — и продолжает
|
|
55
|
+
// видеть то, что должна. Статичный флаг исключения в рецепте спрятал бы образец от
|
|
56
|
+
// приёмки, и запись прошла бы зелёной на красном.
|
|
57
|
+
const recipes = rec.recipes && typeof rec.recipes === "object" ? rec.recipes : {};
|
|
58
|
+
const isPortable = picked === String(recipes.any || "");
|
|
59
|
+
if (!isPortable) cmd = `bash ${PROJECT_GATES}/_native.sh . ${cmd}`;
|
|
44
60
|
|
|
45
61
|
const manPath = join(CWD, MANIFEST);
|
|
46
62
|
const { text, why } = manifestWithGate(await readFile(manPath, "utf8"), slug, cmd);
|
|
@@ -50,39 +66,39 @@ async function installGate(slug, man, facts) {
|
|
|
50
66
|
|
|
51
67
|
async function cmdAdd(args) {
|
|
52
68
|
const slug = args.find((a) => !a.startsWith("-"));
|
|
53
|
-
if (!slug) die(
|
|
69
|
+
if (!slug) die(L.add.needName(`${SELF} add ${L.help.name}`, `${SELF} doctor`));
|
|
54
70
|
|
|
55
71
|
const man = await readManifest();
|
|
56
|
-
if (!man) die(
|
|
72
|
+
if (!man) die(L.add.noManifest(`${SELF} init`));
|
|
57
73
|
const facts = await detectFacts(man);
|
|
58
74
|
|
|
59
75
|
const src = join(GATES_SRC, slug);
|
|
60
|
-
if (!(await exists(src))) die(
|
|
76
|
+
if (!(await exists(src))) die(L.add.noSuchGate(slug, `${SELF} doctor`));
|
|
61
77
|
const probe = { slug, ...parseManifest(await readFile(join(src, "gate.yml"), "utf8")) };
|
|
62
78
|
const verdict = triggerVerdict(probe, facts);
|
|
63
79
|
if (!verdict.applies) {
|
|
64
|
-
console.log(c.yellow(`\n
|
|
65
|
-
console.log(c.dim(
|
|
80
|
+
console.log(c.yellow(`\n ${L.add.notApplicable(verdict.why)}`));
|
|
81
|
+
console.log(c.dim(` ${L.add.installAnyway}\n`));
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
const { cmd, copied, declared, why } = await installGate(slug, man, facts);
|
|
69
85
|
|
|
70
86
|
console.log(c.bold(`\naqk add ${slug}\n`));
|
|
71
|
-
console.log(` ${c.green("✔")} ${PROJECT_GATES}/${slug}/ ${c.dim(
|
|
87
|
+
console.log(` ${c.green("✔")} ${PROJECT_GATES}/${slug}/ ${c.dim(L.add.copied(copied.length))}`);
|
|
72
88
|
if (declared) {
|
|
73
|
-
console.log(` ${c.green("✔")} .aqk.yml ${c.dim(
|
|
89
|
+
console.log(` ${c.green("✔")} .aqk.yml ${c.dim(L.add.declared(cmd))}`);
|
|
74
90
|
} else {
|
|
75
|
-
console.log(` ${c.yellow("!")} .aqk.yml ${c.dim(
|
|
91
|
+
console.log(` ${c.yellow("!")} .aqk.yml ${c.dim(L.add.notDeclared(why, slug, cmd))}`);
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
console.log(`
|
|
79
|
-
${c.bold(
|
|
95
|
+
${c.bold(L.add.nextTitle)}
|
|
80
96
|
|
|
81
|
-
1.
|
|
82
|
-
${c.bold(`${cmd.replace(/ \.$/, ` ${PROJECT_GATES}/${slug}/red`)}`)} ${c.dim(
|
|
83
|
-
${c.bold(`${cmd.replace(/ \.$/, ` ${PROJECT_GATES}/${slug}/green`)}`)} ${c.dim(
|
|
84
|
-
2.
|
|
85
|
-
3.
|
|
97
|
+
1. ${L.add.next1}
|
|
98
|
+
${c.bold(`${cmd.replace(/ \.$/, ` ${PROJECT_GATES}/${slug}/red`)}`)} ${c.dim(L.add.expectFail)}
|
|
99
|
+
${c.bold(`${cmd.replace(/ \.$/, ` ${PROJECT_GATES}/${slug}/green`)}`)} ${c.dim(L.add.expectSilence)}
|
|
100
|
+
2. ${L.add.next2} ${c.dim(L.add.next2Why)}
|
|
101
|
+
3. ${L.add.next3(c.bold(`${SELF} doctor --run`))}
|
|
86
102
|
`);
|
|
87
103
|
}
|
|
88
104
|
|
|
@@ -92,9 +108,9 @@ ${c.bold("Дальше:")}
|
|
|
92
108
|
|
|
93
109
|
async function cmdNew(args) {
|
|
94
110
|
const slug = args.find((a) => !a.startsWith("-"));
|
|
95
|
-
if (!slug) die(
|
|
111
|
+
if (!slug) die(L.gnew.needName(`${SELF} new no-print-in-prod`));
|
|
96
112
|
if (!/^[a-z][a-z0-9-]{2,}$/.test(slug)) {
|
|
97
|
-
die(
|
|
113
|
+
die(L.gnew.badName(slug));
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
// Сначала сверка: новая запись нужна реже, чем кажется. Порог берётся по совпадению с
|
|
@@ -102,10 +118,10 @@ async function cmdNew(args) {
|
|
|
102
118
|
const words = slug.replace(/-/g, " ") + " " + args.filter((a) => !a.startsWith("-")).slice(1).join(" ");
|
|
103
119
|
for (const { rec, hits, headScore } of await matchCatalog(words)) {
|
|
104
120
|
if (hits >= 2 && headScore >= 0.5 && !args.includes("--force")) {
|
|
105
|
-
console.log(c.yellow(`\n
|
|
121
|
+
console.log(c.yellow(`\n ${L.gnew.looksExisting(c.bold(rec.slug))}`));
|
|
106
122
|
console.log(` ${rec.intent || ""}\n`);
|
|
107
|
-
console.log(c.dim(
|
|
108
|
-
console.log(c.dim(`
|
|
123
|
+
console.log(c.dim(` ${L.gnew.recipeNotGate}`));
|
|
124
|
+
console.log(c.dim(` ${L.gnew.forceHint(`${SELF} new ${slug} --force`)}\n`));
|
|
109
125
|
process.exit(1);
|
|
110
126
|
}
|
|
111
127
|
}
|
|
@@ -116,7 +132,7 @@ async function cmdNew(args) {
|
|
|
116
132
|
// результата нет. Признак один — работаем ли мы над самим комплектом.
|
|
117
133
|
const inKit = resolve(CWD) === resolve(PKG_ROOT);
|
|
118
134
|
const dst = inKit ? join(GATES_SRC, slug) : join(CWD, PROJECT_GATES, slug);
|
|
119
|
-
if (await exists(dst)) die(
|
|
135
|
+
if (await exists(dst)) die(L.gnew.exists(relative(CWD, dst)));
|
|
120
136
|
|
|
121
137
|
await mkdir(join(dst, "red"), { recursive: true });
|
|
122
138
|
await mkdir(join(dst, "green"), { recursive: true });
|
|
@@ -137,17 +153,17 @@ async function cmdNew(args) {
|
|
|
137
153
|
console.log(c.bold(`\naqk new ${slug}\n`));
|
|
138
154
|
console.log(` ${c.green("✔")} ${relative(CWD, dst)}/ ${c.dim("gate.yml · check.sh · red/ · green/ · README.md")}`);
|
|
139
155
|
console.log(`
|
|
140
|
-
${c.bold(
|
|
141
|
-
|
|
142
|
-
1. ${c.bold(
|
|
143
|
-
${c.dim(
|
|
144
|
-
2. ${c.bold(
|
|
145
|
-
|
|
146
|
-
${c.dim(
|
|
147
|
-
3. ${c.bold(
|
|
148
|
-
4. ${c.bold(
|
|
149
|
-
5. ${c.bold(
|
|
150
|
-
${c.dim(
|
|
156
|
+
${c.bold(L.gnew.nextTitle)}
|
|
157
|
+
|
|
158
|
+
1. ${c.bold(L.gnew.n1)} ${L.gnew.n1Where}
|
|
159
|
+
${c.dim(L.gnew.n1Why)}
|
|
160
|
+
2. ${c.bold(L.gnew.n2)} ${c.bold("red/")} ${L.gnew.n2Red}
|
|
161
|
+
${c.bold("green/")} ${L.gnew.n2Green}
|
|
162
|
+
${c.dim(L.gnew.n2Why)}
|
|
163
|
+
3. ${c.bold(L.gnew.n3)} ${L.gnew.n3Where}
|
|
164
|
+
4. ${c.bold(L.gnew.n4)} ${L.gnew.n4What}
|
|
165
|
+
5. ${c.bold(L.gnew.n5)} bash tool/selfcheck/gates.sh
|
|
166
|
+
${c.dim(L.gnew.n5Why)}
|
|
151
167
|
`);
|
|
152
168
|
}
|
|
153
169
|
|
|
@@ -159,14 +175,14 @@ ${c.bold("Дальше — по порядку:")}
|
|
|
159
175
|
|
|
160
176
|
async function cmdRatchet(args) {
|
|
161
177
|
const slug = args.find((a) => !a.startsWith("-"));
|
|
162
|
-
if (!slug) die(
|
|
178
|
+
if (!slug) die(L.ratchet.needName(`${SELF} ratchet ${L.help.name}`));
|
|
163
179
|
|
|
164
180
|
const manPath = join(CWD, MANIFEST);
|
|
165
|
-
if (!(await exists(manPath))) die(
|
|
181
|
+
if (!(await exists(manPath))) die(L.ratchet.noManifest(`${SELF} init`));
|
|
166
182
|
let text = await readFile(manPath, "utf8");
|
|
167
183
|
|
|
168
184
|
const line = text.split("\n").find((l) => new RegExp(`^\\s+${slug}:`).test(l));
|
|
169
|
-
if (!line) die(
|
|
185
|
+
if (!line) die(L.ratchet.notDeclared(slug, `${SELF} add ${slug}`));
|
|
170
186
|
|
|
171
187
|
const cmd = line.replace(/^\s*[^:]+:\s*/, "").replace(/^"|"$/g, "");
|
|
172
188
|
const inKit = resolve(CWD) === resolve(PKG_ROOT);
|
|
@@ -177,7 +193,7 @@ async function cmdRatchet(args) {
|
|
|
177
193
|
// Тогда снимаем снимок заново по внутренней команде, а строку манифеста не трогаем.
|
|
178
194
|
const reg = join(CWD, RATCHET_DIR, `${slug}.txt`);
|
|
179
195
|
const wrapped0 = cmd.includes("ratchet.sh");
|
|
180
|
-
if (wrapped0 && (await exists(reg))) die(
|
|
196
|
+
if (wrapped0 && (await exists(reg))) die(L.ratchet.already(slug));
|
|
181
197
|
const prefix = `bash ${lib} ${RATCHET_DIR}/${slug}.txt `;
|
|
182
198
|
const inner = wrapped0 && cmd.startsWith(prefix) ? cmd.slice(prefix.length) : cmd;
|
|
183
199
|
|
|
@@ -193,11 +209,7 @@ async function cmdRatchet(args) {
|
|
|
193
209
|
// строки не должна читаться как новое нарушение.
|
|
194
210
|
const r = spawnSync(inner, { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
195
211
|
if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
|
|
196
|
-
die(
|
|
197
|
-
`Гейт «${slug}» не запускается: ${inner}\n` +
|
|
198
|
-
`Снимать долг с несуществующего сторожа нельзя — в реестр попадут его же сообщения\n` +
|
|
199
|
-
`об ошибке, и он станет разрешением. Сначала почини команду.`
|
|
200
|
-
);
|
|
212
|
+
die(L.ratchet.notRunnable(slug, inner));
|
|
201
213
|
}
|
|
202
214
|
const keys = [...new Set(
|
|
203
215
|
`${r.stdout || ""}${r.stderr || ""}`
|
|
@@ -210,10 +222,7 @@ async function cmdRatchet(args) {
|
|
|
210
222
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
211
223
|
await writeFile(
|
|
212
224
|
reg,
|
|
213
|
-
|
|
214
|
-
`# Снят ${stamp}. Список разрешается ТОЛЬКО укорачивать.\n` +
|
|
215
|
-
`# Новое нарушение красит гейт; исправленное вычёркивается автоматически.\n` +
|
|
216
|
-
keys.join("\n") + (keys.length ? "\n" : ""),
|
|
225
|
+
L.ratchet.registryHead(slug, stamp) + keys.join("\n") + (keys.length ? "\n" : ""),
|
|
217
226
|
"utf8"
|
|
218
227
|
);
|
|
219
228
|
|
|
@@ -224,19 +233,19 @@ async function cmdRatchet(args) {
|
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
console.log(c.bold(`\naqk ratchet ${slug}\n`));
|
|
227
|
-
console.log(` ${c.green("✔")} ${RATCHET_DIR}/${slug}.txt ${c.dim(
|
|
228
|
-
if (!inKit) console.log(` ${c.green("✔")} ${lib} ${c.dim(
|
|
229
|
-
console.log(` ${c.green("✔")} .aqk.yml ${c.dim(
|
|
236
|
+
console.log(` ${c.green("✔")} ${RATCHET_DIR}/${slug}.txt ${c.dim(L.ratchet.recorded(keys.length))}`);
|
|
237
|
+
if (!inKit) console.log(` ${c.green("✔")} ${lib} ${c.dim(L.ratchet.libCopied)}`);
|
|
238
|
+
console.log(` ${c.green("✔")} .aqk.yml ${c.dim(L.ratchet.wrapped)}`);
|
|
230
239
|
console.log(`
|
|
231
|
-
${c.bold(
|
|
240
|
+
${c.bold(L.ratchet.changesTitle)}
|
|
232
241
|
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
${L.ratchet.changes1(c.bold(L.ratchet.fromToday))}
|
|
243
|
+
${L.ratchet.changes2}
|
|
235
244
|
|
|
236
|
-
${c.dim(
|
|
237
|
-
${c.dim(
|
|
245
|
+
${c.dim(L.ratchet.test1)}
|
|
246
|
+
${c.dim(L.ratchet.test2)}
|
|
238
247
|
|
|
239
|
-
|
|
248
|
+
${L.ratchet.run(c.bold(`${SELF} doctor --run`))}
|
|
240
249
|
`);
|
|
241
250
|
}
|
|
242
251
|
|
|
@@ -250,7 +259,7 @@ ${c.bold("Что это меняет:")}
|
|
|
250
259
|
|
|
251
260
|
async function cmdFind(args) {
|
|
252
261
|
const query = args.filter((a) => !a.startsWith("-")).join(" ").trim();
|
|
253
|
-
if (!query) die(
|
|
262
|
+
if (!query) die(L.find.needQuery(`${SELF} ${L.find.example}`));
|
|
254
263
|
|
|
255
264
|
const q = stems(query);
|
|
256
265
|
const scored = (await matchCatalog(query)).map((m) => [m.score, m.rec]);
|
|
@@ -273,41 +282,41 @@ async function cmdFind(args) {
|
|
|
273
282
|
const near = scored.filter(([sc]) => sc >= 0.3 && sc < 0.6);
|
|
274
283
|
|
|
275
284
|
if (same.length) {
|
|
276
|
-
console.log(c.green(
|
|
285
|
+
console.log(c.green(` ${L.find.exists}\n`));
|
|
277
286
|
for (const [sc, rec] of same.slice(0, 3)) {
|
|
278
|
-
console.log(` ${c.bold(rec.slug)} ${c.dim(
|
|
287
|
+
console.log(` ${c.bold(rec.slug)} ${c.dim(L.find.match(Math.round(sc * 100)))}`);
|
|
279
288
|
console.log(` ${rec.intent || ""}`);
|
|
280
289
|
const langs = Object.keys(rec.recipes || {}).filter((k) => k !== "any");
|
|
281
|
-
console.log(c.dim(`
|
|
290
|
+
console.log(c.dim(` ${L.find.recipes(langs.length ? langs.join(", ") + ", " : "")}`));
|
|
282
291
|
}
|
|
283
|
-
console.log(c.dim(
|
|
284
|
-
console.log(c.dim(
|
|
292
|
+
console.log(c.dim(`\n ${L.find.existsWhy1}`));
|
|
293
|
+
console.log(c.dim(` ${L.find.existsWhy2}\n`));
|
|
285
294
|
} else if (near.length) {
|
|
286
|
-
console.log(c.yellow(
|
|
295
|
+
console.log(c.yellow(` ${L.find.near}\n`));
|
|
287
296
|
for (const [sc, rec] of near.slice(0, 4)) {
|
|
288
297
|
console.log(` ${c.bold(rec.slug)} ${c.dim(`${Math.round(sc * 100)}%`)} ${rec.intent || ""}`);
|
|
289
298
|
}
|
|
290
|
-
console.log(c.dim(
|
|
299
|
+
console.log(c.dim(`\n ${L.find.nearWhy}\n`));
|
|
291
300
|
} else {
|
|
292
|
-
console.log(c.yellow(
|
|
301
|
+
console.log(c.yellow(` ${L.find.none}\n`));
|
|
293
302
|
}
|
|
294
303
|
|
|
295
304
|
if (journal.length) {
|
|
296
|
-
console.log(c.bold(
|
|
305
|
+
console.log(c.bold(` ${L.find.journal}\n`));
|
|
297
306
|
for (const [, date, title] of journal.slice(0, 3)) console.log(` ${c.dim(date)} ${title}`);
|
|
298
|
-
console.log(c.dim(
|
|
307
|
+
console.log(c.dim(`\n ${L.find.journalWhy}\n`));
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
if (!same.length) {
|
|
302
|
-
console.log(`${c.bold(
|
|
303
|
-
|
|
304
|
-
1. ${c.bold(
|
|
305
|
-
${c.dim(
|
|
306
|
-
2. ${c.bold(
|
|
307
|
-
${c.dim(
|
|
308
|
-
3. ${c.bold(
|
|
309
|
-
${c.dim(
|
|
310
|
-
4. ${c.bold(
|
|
311
|
+
console.log(`${c.bold(L.find.howTitle)}
|
|
312
|
+
|
|
313
|
+
1. ${c.bold(L.find.how1)} ${L.find.how1What}
|
|
314
|
+
${c.dim(L.find.how1Why)}
|
|
315
|
+
2. ${c.bold(L.find.how2)} kit/gates/${L.help.name}/ ${L.find.how2What}
|
|
316
|
+
${c.dim(L.find.how2Why)}
|
|
317
|
+
3. ${c.bold(L.find.how3)} bash tool/selfcheck/gates.sh
|
|
318
|
+
${c.dim(L.find.how3Why)}
|
|
319
|
+
4. ${c.bold(L.find.how4)} ${L.find.how4What}
|
|
311
320
|
`);
|
|
312
321
|
}
|
|
313
322
|
}
|
|
@@ -340,15 +349,15 @@ async function runsInCi(slug, cmd) {
|
|
|
340
349
|
const script = (cmd.match(/[\w./-]+\.(?:sh|mjs|js|py)/) || [])[0];
|
|
341
350
|
for (const f of files) {
|
|
342
351
|
const text = await readFile(f, "utf8");
|
|
343
|
-
if (/doctor\s+--run|--run\s+.*doctor/.test(text)) return { ci: true, runs: true, how:
|
|
344
|
-
if (text.includes(slug) || (script && text.includes(script))) return { ci: true, runs: true, how:
|
|
352
|
+
if (/doctor\s+--run|--run\s+.*doctor/.test(text)) return { ci: true, runs: true, how: L.why.ciAtOnce };
|
|
353
|
+
if (text.includes(slug) || (script && text.includes(script))) return { ci: true, runs: true, how: L.why.ciOwnStep };
|
|
345
354
|
}
|
|
346
355
|
return { ci: true, runs: false };
|
|
347
356
|
}
|
|
348
357
|
|
|
349
358
|
async function cmdWhy(args) {
|
|
350
359
|
const query = args.filter((a) => !a.startsWith("-")).join(" ").trim();
|
|
351
|
-
if (!query) die(
|
|
360
|
+
if (!query) die(L.why.needQuery(`${SELF} ${L.why.example}`));
|
|
352
361
|
|
|
353
362
|
const man = await readManifest();
|
|
354
363
|
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
@@ -365,80 +374,80 @@ async function cmdWhy(args) {
|
|
|
365
374
|
// случай отправляет чинить не то, а это дороже, чем лишний вопрос.
|
|
366
375
|
const near = matches.filter((x) => x.rawScore >= 0.18).sort((a, b) => b.rawScore - a.rawScore);
|
|
367
376
|
if (!byName && (!best || best.score < 0.5) && near.length) {
|
|
368
|
-
console.log(c.yellow(
|
|
377
|
+
console.log(c.yellow(` ${L.why.unsure}\n`));
|
|
369
378
|
for (const m of near.slice(0, 3)) {
|
|
370
379
|
console.log(` ${c.bold(m.rec.slug)} ${c.dim(`${Math.round(m.rawScore * 100)}%`)} ${m.rec.intent || ""}`);
|
|
371
380
|
}
|
|
372
|
-
console.log(c.dim(`\n
|
|
373
|
-
console.log(c.dim(`
|
|
381
|
+
console.log(c.dim(`\n ${L.why.unsureByName(`${SELF} why ${L.help.name}`)}`));
|
|
382
|
+
console.log(c.dim(` ${L.why.unsureNone(`${SELF} new ${L.help.name}`)}\n`));
|
|
374
383
|
return;
|
|
375
384
|
}
|
|
376
385
|
|
|
377
386
|
const decide = () => {
|
|
378
|
-
console.log(`${c.bold(
|
|
379
|
-
console.log(c.dim(
|
|
380
|
-
console.log(c.dim(`
|
|
387
|
+
console.log(`${c.bold(L.why.decideTitle)} ${L.why.decideQ}`);
|
|
388
|
+
console.log(c.dim(` ${L.why.decideWhy}`));
|
|
389
|
+
console.log(c.dim(` ${L.why.decideNote(`${SELF} note "…"`)}\n`));
|
|
381
390
|
};
|
|
382
391
|
|
|
383
392
|
// --- 1. сторожа не было ----------------------------------------------------
|
|
384
393
|
if (!best || best.score < 0.25) {
|
|
385
|
-
console.log(c.yellow(
|
|
386
|
-
console.log(` ${c.bold(
|
|
387
|
-
console.log(c.dim(
|
|
394
|
+
console.log(c.yellow(` ${L.why.noGuard}`) + c.dim(` ${L.why.noGuardWhy}\n`));
|
|
395
|
+
console.log(` ${c.bold(L.why.fix)} ${L.why.noGuardFix(c.bold(`${SELF} new ${L.help.name}`))}`);
|
|
396
|
+
console.log(c.dim(` ${L.why.noGuardHint}\n`));
|
|
388
397
|
decide();
|
|
389
398
|
return;
|
|
390
399
|
}
|
|
391
400
|
|
|
392
401
|
const slug = best.rec.slug;
|
|
393
|
-
console.log(`
|
|
402
|
+
console.log(` ${L.why.closest(c.bold(slug), Math.round(best.score * 100))}`);
|
|
394
403
|
console.log(` ${c.dim(best.rec.intent || "")}\n`);
|
|
395
404
|
|
|
396
405
|
// --- 2. запись есть, но в проекте не объявлена -----------------------------
|
|
397
406
|
const cmd = gates[slug];
|
|
398
407
|
if (!cmd || !String(cmd).trim()) {
|
|
399
|
-
console.log(c.yellow(
|
|
400
|
-
console.log(` ${c.bold(
|
|
401
|
-
console.log(c.dim(
|
|
402
|
-
console.log(c.dim(`
|
|
408
|
+
console.log(c.yellow(` ${L.why.notInstalled}\n`));
|
|
409
|
+
console.log(` ${c.bold(L.why.fix)} ${c.bold(`${SELF} add ${slug}`)}`);
|
|
410
|
+
console.log(c.dim(` ${L.why.notInstalledHint1}`));
|
|
411
|
+
console.log(c.dim(` ${L.why.notInstalledHint2(`${SELF} ratchet ${slug}`)}\n`));
|
|
403
412
|
decide();
|
|
404
413
|
return;
|
|
405
414
|
}
|
|
406
415
|
|
|
407
416
|
// --- 3. объявлен: спрашиваем у него самого ---------------------------------
|
|
408
|
-
console.log(c.dim(`
|
|
417
|
+
console.log(c.dim(` ${L.why.declaredAs(cmd)}`));
|
|
409
418
|
const r = spawnSync(String(cmd), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
410
419
|
const ci = await runsInCi(slug, String(cmd));
|
|
411
420
|
|
|
412
421
|
if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
|
|
413
|
-
console.log(c.yellow(
|
|
414
|
-
console.log(` ${c.bold(
|
|
415
|
-
console.log(c.dim(
|
|
422
|
+
console.log(c.yellow(`\n ${L.why.notRunning}`) + c.dim(` ${L.why.notRunningWhy}\n`));
|
|
423
|
+
console.log(` ${c.bold(L.why.fix)} ${L.why.notRunningFix}`);
|
|
424
|
+
console.log(c.dim(` ${L.why.notRunningHint}\n`));
|
|
416
425
|
decide();
|
|
417
426
|
return;
|
|
418
427
|
}
|
|
419
428
|
|
|
420
429
|
if (r.status !== 0) {
|
|
421
|
-
console.log(c.yellow(
|
|
430
|
+
console.log(c.yellow(`\n ${L.why.bypassed}\n`));
|
|
422
431
|
if (!ci.ci) {
|
|
423
|
-
console.log(` ${c.bold(
|
|
424
|
-
console.log(c.dim(
|
|
432
|
+
console.log(` ${c.bold(L.why.fix)} ${L.why.noCiFix}`);
|
|
433
|
+
console.log(c.dim(` ${L.why.noCiHint}\n`));
|
|
425
434
|
} else if (!ci.runs) {
|
|
426
|
-
console.log(` ${c.bold(
|
|
427
|
-
console.log(c.dim(`
|
|
435
|
+
console.log(` ${c.bold(L.why.fix)} ${L.why.notInCiFix}`);
|
|
436
|
+
console.log(c.dim(` ${L.why.notInCiHint(`${SELF} doctor --run`)}\n`));
|
|
428
437
|
} else {
|
|
429
|
-
console.log(` ${c.bold(
|
|
430
|
-
console.log(c.dim(
|
|
431
|
-
console.log(c.dim(
|
|
438
|
+
console.log(` ${c.bold(L.why.fix)} ${L.why.inCiFix(ci.how)}`);
|
|
439
|
+
console.log(c.dim(` ${L.why.inCiHint1}`));
|
|
440
|
+
console.log(c.dim(` ${L.why.inCiHint2}\n`));
|
|
432
441
|
}
|
|
433
442
|
decide();
|
|
434
443
|
return;
|
|
435
444
|
}
|
|
436
445
|
|
|
437
|
-
console.log(c.yellow(
|
|
438
|
-
console.log(` ${c.bold(
|
|
439
|
-
console.log(c.dim(
|
|
440
|
-
console.log(c.dim(
|
|
441
|
-
console.log(c.dim(`
|
|
446
|
+
console.log(c.yellow(`\n ${L.why.blind}\n`));
|
|
447
|
+
console.log(` ${c.bold(L.why.fix)} ${L.why.blindFix(c.bold(`${slug}/red/`))}`);
|
|
448
|
+
console.log(c.dim(` ${L.why.blindHint1}`));
|
|
449
|
+
console.log(c.dim(` ${L.why.blindHint2}\n`));
|
|
450
|
+
console.log(c.dim(` ${L.why.blindCheck}\n`));
|
|
442
451
|
decide();
|
|
443
452
|
}
|
|
444
453
|
|