wendkeep 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/bin/wendkeep.mjs +1 -1
- package/hooks/linked-notes.mjs +62 -38
- package/package.json +1 -1
- package/src/companion-select.mjs +5 -4
- package/src/init.mjs +42 -6
- package/src/sensors.mjs +48 -16
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.9.1] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Interactive install UX: language first.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`wendkeep init` asks the vault language first** on an interactive TTY (when `--locale`
|
|
13
|
+
isn't passed): `[1] Português [2] English`. The answer drives the folders, scaffold and
|
|
14
|
+
skills — and the remaining prompts (vault path, companion selection) now render in the
|
|
15
|
+
chosen locale instead of always Portuguese. `--yes`, `--locale` and non-TTY are unchanged.
|
|
16
|
+
|
|
17
|
+
## [0.9.0] — 2026-07-06
|
|
18
|
+
|
|
19
|
+
Engineering debt: sensor editing + i18n coherence for auto-generated notes.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`wendkeep sensors add <id> "<command>"`** (`--severity` / `--type` / `--report` / `--name`
|
|
23
|
+
/ `--description`) — append a sensor to `wendkeep.sensors.json` (creates the file with
|
|
24
|
+
`$schema` when absent, dedups by id) instead of hand-editing JSON.
|
|
25
|
+
- **Locale-aware derived notes:** the auto-generated bug/decision/learning notes render their
|
|
26
|
+
headings + callout in the vault locale — an `en` vault no longer gets Portuguese headings.
|
|
27
|
+
|
|
28
|
+
### Deferred (with reason)
|
|
29
|
+
- `migrate-locale`: renaming a populated vault breaks every wikilink to the old folder names;
|
|
30
|
+
needs a backlink-repair pass — its own effort, not a patch.
|
|
31
|
+
- Code-hash verdict freshness: a change carries no file manifest, so "the code" is undefined;
|
|
32
|
+
the existing `tarefas.md` hash already blocks task drift.
|
|
33
|
+
|
|
7
34
|
## [0.8.1] — 2026-07-06
|
|
8
35
|
|
|
9
36
|
Polish: i18n coherence + presentation.
|
|
@@ -196,6 +223,8 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
196
223
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
197
224
|
|
|
198
225
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
226
|
+
[0.9.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.1
|
|
227
|
+
[0.9.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.0
|
|
199
228
|
[0.8.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.1
|
|
200
229
|
[0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
|
|
201
230
|
[0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -46,7 +46,7 @@ Usage:
|
|
|
46
46
|
wendkeep change <sub> Change lifecycle: new [--simple] | list | show | status |
|
|
47
47
|
done <id> | undone <id> | diff | archive [--force].
|
|
48
48
|
wendkeep spec <sub> Living specs: list | show <capability>.
|
|
49
|
-
wendkeep sensors list
|
|
49
|
+
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
50
50
|
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
51
51
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
52
52
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -162,7 +162,28 @@ export function extractBugDetails(tx) {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
// Locale labels for the auto-generated derived notes (0.9.0). Output-only — the extraction
|
|
166
|
+
// heuristics are untouched. Default pt-BR keeps existing behaviour for every legacy caller.
|
|
167
|
+
const NOTE_LABELS = {
|
|
168
|
+
'pt-BR': {
|
|
169
|
+
autoTag: 'Auto-gerada', autoLine: (p) => `Nota criada automaticamente pelo hook Stop do ${p}.`, session: 'Sessão',
|
|
170
|
+
verify: '_Extraído da sessão — verificar._', complete: '_Extraído da sessão — complementar._',
|
|
171
|
+
bug: { symptom: 'Sintoma', rootCause: 'Causa raiz', fix: 'Correção', files: 'Arquivos alterados', evidence: 'Evidência', lessons: 'Lições aprendidas', noFix: '_Nenhuma correção explícita detectada._', seeSession: '_Ver sessão vinculada._', addEvidence: '_Adicionar evidência empírica._' },
|
|
172
|
+
dec: { context: 'Contexto', decision: 'Decisão', consequences: 'Consequências', alternatives: 'Alternativas consideradas', noAlt: '_Nenhuma alternativa registrada automaticamente._' },
|
|
173
|
+
learn: { title: 'Aprendizado', context: 'Contexto', learned: 'O que aprendemos', future: 'Como aplicar no futuro', futureHint: '_Registrar como este conhecimento pode ser reutilizado._' },
|
|
174
|
+
},
|
|
175
|
+
en: {
|
|
176
|
+
autoTag: 'Auto-generated', autoLine: (p) => `Note created automatically by the ${p} Stop hook.`, session: 'Session',
|
|
177
|
+
verify: '_Extracted from the session — verify._', complete: '_Extracted from the session — complete._',
|
|
178
|
+
bug: { symptom: 'Symptom', rootCause: 'Root cause', fix: 'Fix', files: 'Changed files', evidence: 'Evidence', lessons: 'Lessons learned', noFix: '_No explicit fix detected._', seeSession: '_See the linked session._', addEvidence: '_Add empirical evidence._' },
|
|
179
|
+
dec: { context: 'Context', decision: 'Decision', consequences: 'Consequences', alternatives: 'Alternatives considered', noAlt: '_No alternative recorded automatically._' },
|
|
180
|
+
learn: { title: 'Learning', context: 'Context', learned: 'What we learned', future: 'How to apply in future', futureHint: '_Record how this knowledge can be reused._' },
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
function noteLabels(localeId) { return NOTE_LABELS[localeId] || NOTE_LABELS['pt-BR']; }
|
|
184
|
+
|
|
185
|
+
export function buildBugNoteContent(bug, issueRef, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(bug.rootCause), localeId = 'pt-BR') {
|
|
186
|
+
const L = noteLabels(localeId);
|
|
166
187
|
const title = issueRef
|
|
167
188
|
? `${issueRef} - ${normalizeInline(bug.rootCause, 80)}`
|
|
168
189
|
: normalizeInline(bug.rootCause, 80);
|
|
@@ -184,31 +205,31 @@ issue: ${yamlQuote(issueRef || '')}
|
|
|
184
205
|
|
|
185
206
|
# Bug - ${title}
|
|
186
207
|
|
|
187
|
-
> [!note]
|
|
188
|
-
>
|
|
189
|
-
>
|
|
208
|
+
> [!note] ${L.autoTag}
|
|
209
|
+
> ${L.autoLine(provider.label)}
|
|
210
|
+
> ${L.session}: ${wikilinkFromRel(sessionRel)}
|
|
190
211
|
|
|
191
|
-
##
|
|
212
|
+
## ${L.bug.symptom}
|
|
192
213
|
|
|
193
|
-
${bug.symptom ||
|
|
214
|
+
${bug.symptom || L.verify}
|
|
194
215
|
|
|
195
|
-
##
|
|
216
|
+
## ${L.bug.rootCause}
|
|
196
217
|
|
|
197
218
|
${bug.rootCause}
|
|
198
219
|
|
|
199
|
-
##
|
|
220
|
+
## ${L.bug.fix}
|
|
200
221
|
|
|
201
|
-
${markdownList(bug.fixes,
|
|
222
|
+
${markdownList(bug.fixes, L.bug.noFix)}
|
|
202
223
|
|
|
203
|
-
##
|
|
224
|
+
## ${L.bug.files}
|
|
204
225
|
|
|
205
|
-
${markdownList(bug.changedFiles.map((file) => `\`${file}\``),
|
|
226
|
+
${markdownList(bug.changedFiles.map((file) => `\`${file}\``), L.bug.seeSession)}
|
|
206
227
|
|
|
207
|
-
##
|
|
228
|
+
## ${L.bug.evidence}
|
|
208
229
|
|
|
209
|
-
${markdownList(bug.evidence,
|
|
230
|
+
${markdownList(bug.evidence, L.bug.addEvidence)}
|
|
210
231
|
|
|
211
|
-
##
|
|
232
|
+
## ${L.bug.lessons}
|
|
212
233
|
|
|
213
234
|
${bug.lessons}
|
|
214
235
|
`;
|
|
@@ -309,7 +330,8 @@ export function extractDecisionDetails(tx) {
|
|
|
309
330
|
};
|
|
310
331
|
}
|
|
311
332
|
|
|
312
|
-
export function buildDecisionNoteContent(decision, adrNum, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(decision.title)) {
|
|
333
|
+
export function buildDecisionNoteContent(decision, adrNum, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(decision.title), localeId = 'pt-BR') {
|
|
334
|
+
const L = noteLabels(localeId);
|
|
313
335
|
const adrId = `ADR-${String(adrNum).padStart(3, '0')}`;
|
|
314
336
|
return `---
|
|
315
337
|
type: decision
|
|
@@ -327,25 +349,25 @@ superseded_by: ""
|
|
|
327
349
|
|
|
328
350
|
# ${adrId} - ${decision.title}
|
|
329
351
|
|
|
330
|
-
> [!note]
|
|
331
|
-
>
|
|
332
|
-
>
|
|
352
|
+
> [!note] ${L.autoTag}
|
|
353
|
+
> ${L.autoLine(provider.label)}
|
|
354
|
+
> ${L.session}: ${wikilinkFromRel(sessionRel)}
|
|
333
355
|
|
|
334
|
-
##
|
|
356
|
+
## ${L.dec.context}
|
|
335
357
|
|
|
336
|
-
${decision.context ||
|
|
358
|
+
${decision.context || L.complete}
|
|
337
359
|
|
|
338
|
-
##
|
|
360
|
+
## ${L.dec.decision}
|
|
339
361
|
|
|
340
362
|
${decision.detail}
|
|
341
363
|
|
|
342
|
-
##
|
|
364
|
+
## ${L.dec.consequences}
|
|
343
365
|
|
|
344
366
|
${decision.consequences}
|
|
345
367
|
|
|
346
|
-
##
|
|
368
|
+
## ${L.dec.alternatives}
|
|
347
369
|
|
|
348
|
-
${markdownList(decision.alternatives,
|
|
370
|
+
${markdownList(decision.alternatives, L.dec.noAlt)}
|
|
349
371
|
`;
|
|
350
372
|
}
|
|
351
373
|
|
|
@@ -413,7 +435,8 @@ export function extractLearningDetails(tx, bugDetails) {
|
|
|
413
435
|
return learnings.length ? learnings.slice(0, 5) : null;
|
|
414
436
|
}
|
|
415
437
|
|
|
416
|
-
export function buildLearningNoteContent(learning, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(learning.title)) {
|
|
438
|
+
export function buildLearningNoteContent(learning, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(learning.title), localeId = 'pt-BR') {
|
|
439
|
+
const L = noteLabels(localeId);
|
|
417
440
|
return `---
|
|
418
441
|
type: learning
|
|
419
442
|
date: ${dateStr}
|
|
@@ -427,23 +450,23 @@ tags:
|
|
|
427
450
|
${yamlTags(learning.tags.map((tag) => (tag === 'codex' ? provider.tag : tag)))}
|
|
428
451
|
---
|
|
429
452
|
|
|
430
|
-
#
|
|
453
|
+
# ${L.learn.title} - ${learning.title}
|
|
431
454
|
|
|
432
|
-
> [!note]
|
|
433
|
-
>
|
|
434
|
-
>
|
|
455
|
+
> [!note] ${L.autoTag}
|
|
456
|
+
> ${L.autoLine(provider.label)}
|
|
457
|
+
> ${L.session}: ${wikilinkFromRel(sessionRel)}
|
|
435
458
|
|
|
436
|
-
##
|
|
459
|
+
## ${L.learn.context}
|
|
437
460
|
|
|
438
|
-
${learning.context ||
|
|
461
|
+
${learning.context || L.complete}
|
|
439
462
|
|
|
440
|
-
##
|
|
463
|
+
## ${L.learn.learned}
|
|
441
464
|
|
|
442
465
|
${learning.content}
|
|
443
466
|
|
|
444
|
-
##
|
|
467
|
+
## ${L.learn.future}
|
|
445
468
|
|
|
446
|
-
|
|
469
|
+
${L.learn.futureHint}
|
|
447
470
|
`;
|
|
448
471
|
}
|
|
449
472
|
|
|
@@ -478,7 +501,8 @@ function alreadyHasKey(keys, candidate) {
|
|
|
478
501
|
export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options = {}) {
|
|
479
502
|
const linked = { decisions: [], bugs: [], learnings: [] };
|
|
480
503
|
const provider = providerMeta(options.provider);
|
|
481
|
-
const
|
|
504
|
+
const loc = getLocale(vaultBase);
|
|
505
|
+
const locF = loc.folders;
|
|
482
506
|
const bugsDir = join(vaultBase, monthFolderRelFromDateStr(locF.bugs, dateStr, vaultBase));
|
|
483
507
|
const decisionsDir = join(vaultBase, monthFolderRelFromDateStr(locF.decisions, dateStr, vaultBase));
|
|
484
508
|
const learningsDir = join(vaultBase, monthFolderRelFromDateStr(locF.learnings, dateStr, vaultBase));
|
|
@@ -497,7 +521,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
497
521
|
const causeSlug = slugify(bugDetails.rootCause.slice(0, 40), 'bug');
|
|
498
522
|
const fileName = issueRef ? `${issueRef}-${causeSlug}.md` : `${dateStr}-bug-${causeSlug}.md`;
|
|
499
523
|
const filePath = join(bugsDir, fileName);
|
|
500
|
-
if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey), 'utf-8');
|
|
524
|
+
if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey, loc.id), 'utf-8');
|
|
501
525
|
linked.bugs.push(toVaultRelative(vaultBase, filePath));
|
|
502
526
|
existingKeys.bugs.push(bugKey);
|
|
503
527
|
}
|
|
@@ -513,7 +537,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
513
537
|
const filePath = join(decisionsDir, fileName);
|
|
514
538
|
if (!existsSync(filePath)) {
|
|
515
539
|
const adrNum = Number(fileName.match(/^ADR-(\d+)/i)?.[1]) || getNextAdrNumber(vaultBase);
|
|
516
|
-
writeFileSync(filePath, buildDecisionNoteContent(decisionDetails, adrNum, dateStr, sessionRel, provider, decisionKey), 'utf-8');
|
|
540
|
+
writeFileSync(filePath, buildDecisionNoteContent(decisionDetails, adrNum, dateStr, sessionRel, provider, decisionKey, loc.id), 'utf-8');
|
|
517
541
|
}
|
|
518
542
|
linked.decisions.push(toVaultRelative(vaultBase, filePath));
|
|
519
543
|
existingKeys.decisions.push(decisionKey);
|
|
@@ -528,7 +552,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
528
552
|
const learningSlug = slugify(learning.title.slice(0, 40), 'aprendizado');
|
|
529
553
|
const fileName = `${dateStr}-${learningSlug}.md`;
|
|
530
554
|
const filePath = join(learningsDir, fileName);
|
|
531
|
-
if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey), 'utf-8');
|
|
555
|
+
if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey, loc.id), 'utf-8');
|
|
532
556
|
linked.learnings.push(toVaultRelative(vaultBase, filePath));
|
|
533
557
|
existingKeys.learnings.push(learningKey);
|
|
534
558
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/companion-select.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import readline from 'node:readline';
|
|
5
5
|
|
|
6
6
|
const HINT = 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma';
|
|
7
|
+
const HEADER = 'Companions';
|
|
7
8
|
|
|
8
9
|
export function initialCompanionMenu(companions) {
|
|
9
10
|
return {
|
|
@@ -35,8 +36,8 @@ export function reduceCompanionMenu(state, action) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function renderCompanionMenu(state) {
|
|
39
|
-
const lines = [
|
|
39
|
+
export function renderCompanionMenu(state, { hint = HINT, header = HEADER } = {}) {
|
|
40
|
+
const lines = [`${header} — ${hint}:`];
|
|
40
41
|
state.items.forEach((it, i) => {
|
|
41
42
|
const cursor = i === state.cursor ? '>' : ' ';
|
|
42
43
|
const box = it.checked ? '[x]' : '[ ]';
|
|
@@ -63,7 +64,7 @@ export function canInteractiveSelect(input = process.stdin, output = process.std
|
|
|
63
64
|
|
|
64
65
|
// Run the raw-TTY checkbox menu; resolves with the selected ids. Caller must check
|
|
65
66
|
// canInteractiveSelect() first (otherwise use the text fallback).
|
|
66
|
-
export function selectCompanionsInteractive(companions, { input = process.stdin, output = process.stdout } = {}) {
|
|
67
|
+
export function selectCompanionsInteractive(companions, { input = process.stdin, output = process.stdout, labels } = {}) {
|
|
67
68
|
return new Promise((resolve) => {
|
|
68
69
|
let state = initialCompanionMenu(companions);
|
|
69
70
|
let drawnLines = 0;
|
|
@@ -77,7 +78,7 @@ export function selectCompanionsInteractive(companions, { input = process.stdin,
|
|
|
77
78
|
readline.moveCursor(output, 0, -drawnLines);
|
|
78
79
|
readline.clearScreenDown(output);
|
|
79
80
|
}
|
|
80
|
-
const text = renderCompanionMenu(state);
|
|
81
|
+
const text = renderCompanionMenu(state, labels);
|
|
81
82
|
output.write(`${text}\n`);
|
|
82
83
|
drawnLines = text.split('\n').length;
|
|
83
84
|
};
|
package/src/init.mjs
CHANGED
|
@@ -192,17 +192,55 @@ function installVaultColors(vaultPath) {
|
|
|
192
192
|
|
|
193
193
|
// --- main -------------------------------------------------------------------
|
|
194
194
|
|
|
195
|
+
// Interactive prompt strings by locale. The language question itself is bilingual (asked
|
|
196
|
+
// before the locale is known); everything after follows the answer.
|
|
197
|
+
const PROMPTS = {
|
|
198
|
+
'pt-BR': {
|
|
199
|
+
vault: (f) => `Caminho do vault Obsidian (Enter aceita o padrão, ou digite outro)\n [${f}]\n> `,
|
|
200
|
+
companionsHeader: '\nCompanions (plugins/MCP opcionais — context-mode é a principal):',
|
|
201
|
+
companionsAsk: (def) => `Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
|
|
202
|
+
menu: { hint: 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma', header: 'Companions' },
|
|
203
|
+
},
|
|
204
|
+
en: {
|
|
205
|
+
vault: (f) => `Obsidian vault path (Enter for the default, or type another)\n [${f}]\n> `,
|
|
206
|
+
companionsHeader: '\nCompanions (optional plugins/MCP — context-mode is the main one):',
|
|
207
|
+
companionsAsk: (def) => `Enter ids comma-separated (Enter for [${def}], "none" for none): `,
|
|
208
|
+
menu: { hint: 'Space toggles · ↑/↓ move · a=all · n=none · Enter confirms', header: 'Companions' },
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export function promptStrings(localeId) {
|
|
213
|
+
return PROMPTS[localeId] || PROMPTS['pt-BR'];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Map the language answer to a locale id. 2/en → en; 1/pt/empty/unknown → pt-BR.
|
|
217
|
+
export function parseLocaleAnswer(ans) {
|
|
218
|
+
const a = String(ans || '').trim().toLowerCase();
|
|
219
|
+
if (a === '2' || a === 'en' || a === 'english') return 'en';
|
|
220
|
+
return 'pt-BR';
|
|
221
|
+
}
|
|
222
|
+
|
|
195
223
|
export async function runInit(argv) {
|
|
196
224
|
const args = parseArgs(argv);
|
|
197
225
|
const projectPath = resolve(args.project || process.cwd());
|
|
198
226
|
const log = (s) => process.stdout.write(`${s}\n`);
|
|
199
227
|
|
|
228
|
+
// Language first (i18n): an interactive TTY without --locale is asked the vault language;
|
|
229
|
+
// folders, prompts and scaffold all follow the answer.
|
|
230
|
+
if (!args.locale && process.stdin.isTTY && !args.yes) {
|
|
231
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
232
|
+
const ans = await rl.question('Idioma do vault / Vault language:\n [1] Português (pt-BR) [2] English (en)\n> ');
|
|
233
|
+
rl.close();
|
|
234
|
+
args.locale = parseLocaleAnswer(ans);
|
|
235
|
+
}
|
|
236
|
+
const P = promptStrings(args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE);
|
|
237
|
+
|
|
200
238
|
let vaultPath = args.vault;
|
|
201
239
|
if (!vaultPath) {
|
|
202
240
|
const fallback = join(projectPath, deriveVaultDirName(projectPath));
|
|
203
241
|
if (process.stdin.isTTY && !args.yes) {
|
|
204
242
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
205
|
-
const ans = (await rl.question(
|
|
243
|
+
const ans = (await rl.question(P.vault(fallback))).trim();
|
|
206
244
|
rl.close();
|
|
207
245
|
vaultPath = ans || fallback;
|
|
208
246
|
} else {
|
|
@@ -222,16 +260,14 @@ export async function runInit(argv) {
|
|
|
222
260
|
} else if (process.stdin.isTTY && !args.yes) {
|
|
223
261
|
if (canInteractiveSelect()) {
|
|
224
262
|
log(''); // the checkbox menu renders its own header
|
|
225
|
-
companions = await selectCompanionsInteractive(COMPANIONS);
|
|
263
|
+
companions = await selectCompanionsInteractive(COMPANIONS, { labels: P.menu });
|
|
226
264
|
} else {
|
|
227
265
|
// Text fallback (no raw-mode TTY): list + comma input with clear instructions.
|
|
228
|
-
log(
|
|
266
|
+
log(P.companionsHeader);
|
|
229
267
|
for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
|
|
230
268
|
const def = resolveCompanions({}).join(',');
|
|
231
269
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
232
|
-
const ans = (await rl.question(
|
|
233
|
-
`Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
|
|
234
|
-
)).trim();
|
|
270
|
+
const ans = (await rl.question(P.companionsAsk(def))).trim();
|
|
235
271
|
rl.close();
|
|
236
272
|
companions = ans.toLowerCase() === 'none' ? [] : resolveCompanions({ companionsFlag: ans || def });
|
|
237
273
|
}
|
package/src/sensors.mjs
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
|
-
// `wendkeep sensors list
|
|
2
|
-
import {
|
|
1
|
+
// `wendkeep sensors <list|add>` — view/edit wendkeep.sensors.json (0.7.0 / 0.9.0).
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
3
4
|
import { loadSensors } from '../hooks/sensors-core.mjs';
|
|
4
5
|
|
|
6
|
+
const SCHEMA = 'https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json';
|
|
7
|
+
|
|
8
|
+
function opt(argv, name) {
|
|
9
|
+
const i = argv.indexOf(name);
|
|
10
|
+
if (i >= 0) return argv[i + 1];
|
|
11
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
12
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveProject(rest) {
|
|
16
|
+
return resolve(opt(rest, '--project') || process.cwd());
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export function runSensors(argv) {
|
|
6
20
|
const [sub, ...rest] = argv;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (rest[i] === '--project') project = rest[++i];
|
|
14
|
-
else if (rest[i].startsWith('--project=')) project = rest[i].slice(10);
|
|
21
|
+
|
|
22
|
+
if (sub === 'list') {
|
|
23
|
+
const sensors = loadSensors(resolveProject(rest));
|
|
24
|
+
if (!sensors.length) { process.stdout.write('sensors: (nenhum — crie wendkeep.sensors.json na raiz)\n'); process.exit(0); }
|
|
25
|
+
for (const s of sensors) process.stdout.write(`${s.id}: ${s.type || 'command'} · ${s.severity || 'critical'} · ${s.command}\n`);
|
|
26
|
+
process.exit(0);
|
|
15
27
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
|
|
29
|
+
if (sub === 'add') {
|
|
30
|
+
const flags = new Set(['--severity', '--type', '--report', '--name', '--description', '--project']);
|
|
31
|
+
const positional = rest.filter((a, i) => !a.startsWith('--') && !flags.has(rest[i - 1]));
|
|
32
|
+
const [id, command] = positional;
|
|
33
|
+
if (!id || !command) { process.stderr.write('wendkeep sensors add: precisa <id> "<command>"\n'); process.exit(2); }
|
|
34
|
+
const projectRoot = resolveProject(rest);
|
|
35
|
+
const path = join(projectRoot, 'wendkeep.sensors.json');
|
|
36
|
+
let cfg = { $schema: SCHEMA, version: 1, source: 'manual', sensors: [] };
|
|
37
|
+
if (existsSync(path)) {
|
|
38
|
+
try { cfg = JSON.parse(readFileSync(path, 'utf8')); } catch { process.stderr.write('wendkeep sensors add: wendkeep.sensors.json inválido\n'); process.exit(2); }
|
|
39
|
+
if (!Array.isArray(cfg.sensors)) cfg.sensors = [];
|
|
40
|
+
if (!cfg.$schema) cfg.$schema = SCHEMA;
|
|
41
|
+
}
|
|
42
|
+
if (cfg.sensors.some((s) => s.id === id)) { process.stderr.write(`wendkeep sensors add: sensor "${id}" já existe\n`); process.exit(2); }
|
|
43
|
+
const severity = opt(rest, '--severity') === 'warning' ? 'warning' : 'critical';
|
|
44
|
+
const type = opt(rest, '--type') === 'mutation' ? 'mutation' : 'command';
|
|
45
|
+
const sensor = { id, name: opt(rest, '--name') || id, description: opt(rest, '--description') || command, severity, type, command };
|
|
46
|
+
if (type === 'mutation') sensor.report = opt(rest, '--report') || 'reports/mutation/mutation.json';
|
|
47
|
+
cfg.sensors.push(sensor);
|
|
48
|
+
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
|
49
|
+
process.stdout.write(`sensor added: ${id} (${type} · ${severity})\n`);
|
|
50
|
+
process.exit(0);
|
|
21
51
|
}
|
|
22
|
-
|
|
52
|
+
|
|
53
|
+
process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list, add.\n`);
|
|
54
|
+
process.exit(2);
|
|
23
55
|
}
|