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