wendkeep 0.22.0 → 0.23.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/CHANGELOG.md +28 -0
- package/bin/wendkeep.mjs +6 -0
- package/hooks/change-core.mjs +4 -3
- package/hooks/linked-notes.mjs +29 -3
- package/hooks/obsidian-common.mjs +38 -7
- package/package.json +1 -1
- package/src/init.mjs +8 -1
- package/src/vault-views.mjs +99 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ 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.23.0] — 2026-07-08
|
|
8
|
+
|
|
9
|
+
Vault structure — generated views + housekeeping (audit wave 2).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Generated Bases + Dashboard MOC**: `wendkeep init` now writes one folder-filtered `.base`
|
|
13
|
+
per taxonomy area (sessions/decisions/bugs/learnings/specs/changes) and a `00-Dashboard.md`
|
|
14
|
+
that embeds them — the vault's structural index. Filters are **by folder**
|
|
15
|
+
(`file.inFolder("05-Bugs")`), fixing the tag-filter that hid ~1/3 of bugs. New
|
|
16
|
+
`wendkeep dashboard [--force]` (re)generates them; non-destructive (never clobbers your own
|
|
17
|
+
bases). Locale-aware. `src/vault-views.mjs`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Archive ADRs land in the dated month folder** (`04-Decisões/<year>/<MM-MMM>/`) alongside
|
|
21
|
+
session-derived decisions, instead of the year root. `hooks/change-core.mjs`.
|
|
22
|
+
- **`SESSION_REGISTRY` is pruned** on the idle sweep: `done` entries older than 90 days, then a
|
|
23
|
+
cap of 200 most-recent — active entries are never touched. Bounds the per-hook read/serialize
|
|
24
|
+
cost that had grown to 330 entries / ~170 KB in production. `hooks/obsidian-common.mjs`.
|
|
25
|
+
- **Generated note names truncate on a word boundary** instead of mid-word (`slugify` gained a
|
|
26
|
+
boundary-aware `maxLen`). `hooks/obsidian-common.mjs`, `hooks/linked-notes.mjs`.
|
|
27
|
+
- **Learnings dedup vault-wide**: a learning already recorded anywhere in `06-Aprendizados`
|
|
28
|
+
(by `content_key`) is not re-emitted on a later day/session. `hooks/linked-notes.mjs`.
|
|
29
|
+
|
|
30
|
+
### Deferred
|
|
31
|
+
- Unifying the two `buildSessionContent` skeletons (session-start / session-ensure) stays as
|
|
32
|
+
tracked tech-debt — pure refactor, high regression risk in the capture layer, and the
|
|
33
|
+
user-facing drift (`session_id`) was already closed in 0.18/0.21.
|
|
34
|
+
|
|
7
35
|
## [0.22.0] — 2026-07-08
|
|
8
36
|
|
|
9
37
|
Hardening — 10 audit-confirmed bugs fixed (each survived an adversarial refuter).
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -56,6 +56,7 @@ Usage:
|
|
|
56
56
|
--dry-run · --json.
|
|
57
57
|
wendkeep verify [--deep] [--change s] Run a change's task sensors + record evidence (the gate);
|
|
58
58
|
--deep assembles the verification package for the wk-verify pass.
|
|
59
|
+
wendkeep dashboard [--force] (Re)generate the vault's folder-filtered Bases + 00-Dashboard MOC.
|
|
59
60
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
60
61
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
61
62
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
@@ -148,6 +149,11 @@ async function main() {
|
|
|
148
149
|
runImportCli(rest);
|
|
149
150
|
break;
|
|
150
151
|
}
|
|
152
|
+
case 'dashboard': {
|
|
153
|
+
const { runDashboard } = await import('../src/vault-views.mjs');
|
|
154
|
+
runDashboard(rest);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
151
157
|
case '--version':
|
|
152
158
|
case '-v':
|
|
153
159
|
process.stdout.write(`${version()}\n`);
|
package/hooks/change-core.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the `wendkeep change` CLI (src/change.mjs) and the brain-inject hook. No external deps.
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
|
-
import { ensureDir, wikilinkFromRel } from './obsidian-common.mjs';
|
|
6
|
+
import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
7
7
|
import { parseSpecsList, promoteSpecs } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
@@ -261,8 +261,9 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
261
261
|
writeFileSync(pp, c, 'utf8');
|
|
262
262
|
} catch { /* proposta ilegível — segue */ }
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// ADR goes in the same dated month folder as session-derived decisions (04-Decisões/ano/MM-MMM/)
|
|
265
|
+
// — not the year root — so all ADRs sit together in the vault's convention.
|
|
266
|
+
const adrDirRel = monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase);
|
|
266
267
|
ensureDir(join(vaultBase, adrDirRel));
|
|
267
268
|
const num = String(adrNum).padStart(3, '0');
|
|
268
269
|
const adrRel = join(adrDirRel, `ADR-${num}-${slug}.md`);
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -477,6 +477,30 @@ function listMd(dir) {
|
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
// Chaves content_key das derivadas já existentes que linkam esta sessão.
|
|
480
|
+
// Vault-wide learning content_keys (recursive over the learnings folder). existingKeysForSession
|
|
481
|
+
// only looks at the current session + month, so the same lesson re-extracted on a later day/
|
|
482
|
+
// session was duplicated. This dedups a learning against everything already learned in the vault.
|
|
483
|
+
function collectLearningKeys(vaultBase) {
|
|
484
|
+
const keys = new Set();
|
|
485
|
+
const root = join(vaultBase, getLocale(vaultBase).folders.learnings);
|
|
486
|
+
const walk = (d) => {
|
|
487
|
+
let entries;
|
|
488
|
+
try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
489
|
+
for (const e of entries) {
|
|
490
|
+
const p = join(d, e.name);
|
|
491
|
+
if (e.isDirectory()) walk(p);
|
|
492
|
+
else if (e.name.endsWith('.md')) {
|
|
493
|
+
try {
|
|
494
|
+
const m = readFileSync(p, 'utf-8').match(/^content_key:\s*"?(.*?)"?\s*$/m);
|
|
495
|
+
if (m && m[1]) keys.add(m[1]);
|
|
496
|
+
} catch { /* nota ilegível */ }
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
walk(root);
|
|
501
|
+
return keys;
|
|
502
|
+
}
|
|
503
|
+
|
|
480
504
|
function existingKeysForSession(vaultBase, sessionRel, dateStr) {
|
|
481
505
|
const wikilink = wikilinkFromRel(sessionRel);
|
|
482
506
|
const out = { bugs: [], decisions: [], learnings: [] };
|
|
@@ -518,7 +542,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
518
542
|
const issueRef = issueRefs[0] || '';
|
|
519
543
|
const bugKey = derivedContentKey(bugDetails.rootCause);
|
|
520
544
|
if (!alreadyHasKey(existingKeys.bugs, bugKey)) {
|
|
521
|
-
const causeSlug = slugify(bugDetails.rootCause
|
|
545
|
+
const causeSlug = slugify(bugDetails.rootCause, 'bug', 40);
|
|
522
546
|
const fileName = issueRef ? `${issueRef}-${causeSlug}.md` : `${dateStr}-bug-${causeSlug}.md`;
|
|
523
547
|
const filePath = join(bugsDir, fileName);
|
|
524
548
|
if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey, loc.id), 'utf-8');
|
|
@@ -531,7 +555,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
531
555
|
if (decisionDetails) {
|
|
532
556
|
const decisionKey = derivedContentKey(decisionDetails.title);
|
|
533
557
|
if (!alreadyHasKey(existingKeys.decisions, decisionKey)) {
|
|
534
|
-
const titleSlug = slugify(decisionDetails.title
|
|
558
|
+
const titleSlug = slugify(decisionDetails.title, 'decisao', 40);
|
|
535
559
|
const existing = adrFileExistsBySlug(decisionsDir, titleSlug);
|
|
536
560
|
const fileName = existing || `ADR-${String(getNextAdrNumber(vaultBase)).padStart(3, '0')}-${titleSlug}.md`;
|
|
537
561
|
const filePath = join(decisionsDir, fileName);
|
|
@@ -546,10 +570,12 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
546
570
|
|
|
547
571
|
const learnings = extractLearningDetails(tx, bugDetails);
|
|
548
572
|
if (learnings) {
|
|
573
|
+
const vaultLearningKeys = collectLearningKeys(vaultBase); // vault-wide dedup
|
|
549
574
|
for (const learning of learnings) {
|
|
550
575
|
const learningKey = derivedContentKey(learning.title);
|
|
551
576
|
if (alreadyHasKey(existingKeys.learnings, learningKey)) continue;
|
|
552
|
-
|
|
577
|
+
if (vaultLearningKeys.has(learningKey)) continue; // already learned elsewhere in the vault
|
|
578
|
+
const learningSlug = slugify(learning.title, 'aprendizado', 40);
|
|
553
579
|
const fileName = `${dateStr}-${learningSlug}.md`;
|
|
554
580
|
const filePath = join(learningsDir, fileName);
|
|
555
581
|
if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey, loc.id), 'utf-8');
|
|
@@ -289,12 +289,38 @@ export function sweepStaleSessions(registry, nowMs, maxIdleMs, excludeTranscript
|
|
|
289
289
|
return closed;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
//
|
|
292
|
+
// Registry retention. The registry is read/serialized in full on every hook and scanned O(N)
|
|
293
|
+
// for routing — it only needs active + recent sessions (historical audit lives in the notes).
|
|
294
|
+
// Left unbounded it grew to 330 entries / ~170 KB in production.
|
|
295
|
+
export const REGISTRY_KEEP_DONE = 200;
|
|
296
|
+
export const REGISTRY_DONE_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
|
|
297
|
+
|
|
298
|
+
// Pure: drop 'done' entries older than maxAgeMs, then cap the remaining 'done' at keepDone
|
|
299
|
+
// (newest by ended_at/updated_at/started_at kept). Never touches active entries. Mutates the
|
|
300
|
+
// registry and returns how many were pruned.
|
|
301
|
+
export function pruneRegistry(registry, nowMs, { keepDone = REGISTRY_KEEP_DONE, maxAgeMs = REGISTRY_DONE_MAX_AGE_MS } = {}) {
|
|
302
|
+
const sessions = registry?.sessions || {};
|
|
303
|
+
const stamp = (v) => Date.parse((v && (v.ended_at || v.updated_at || v.started_at)) || '') || 0;
|
|
304
|
+
let pruned = 0;
|
|
305
|
+
for (const [id, v] of Object.entries(sessions)) {
|
|
306
|
+
if (!v || v.status !== 'done') continue;
|
|
307
|
+
const t = stamp(v);
|
|
308
|
+
if (t && nowMs - t > maxAgeMs) { delete sessions[id]; pruned += 1; }
|
|
309
|
+
}
|
|
310
|
+
const done = Object.entries(sessions)
|
|
311
|
+
.filter(([, v]) => v && v.status === 'done')
|
|
312
|
+
.sort((a, b) => stamp(b[1]) - stamp(a[1]));
|
|
313
|
+
for (const [id] of done.slice(keepDone)) { delete sessions[id]; pruned += 1; }
|
|
314
|
+
return pruned;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Wrapper de IO: varre as ociosas, poda o registry, grava e fecha a NOTA `.md` de cada
|
|
293
318
|
// sessão encerrada (mantém vault e registry alinhados). Devolve quantas fechou.
|
|
294
319
|
export function sweepStaleSessionsFile(vaultBase, now = new Date(), maxIdleMs = SESSION_IDLE_CLOSE_MS, excludeTranscriptPath = '') {
|
|
295
320
|
const registry = readSessionRegistry(vaultBase);
|
|
296
321
|
const closed = sweepStaleSessions(registry, now.getTime(), maxIdleMs, excludeTranscriptPath);
|
|
297
|
-
|
|
322
|
+
const pruned = pruneRegistry(registry, now.getTime());
|
|
323
|
+
if (closed.length || pruned) writeSessionRegistry(vaultBase, registry);
|
|
298
324
|
for (const { session_file, ended_at } of closed) {
|
|
299
325
|
try { closeSessionNoteFile(vaultBase, session_file, ended_at); } catch { /* nunca derruba o sweep */ }
|
|
300
326
|
}
|
|
@@ -335,15 +361,20 @@ export function closeSessionNote(content, endedAt) {
|
|
|
335
361
|
return next;
|
|
336
362
|
}
|
|
337
363
|
|
|
338
|
-
export function slugify(text, fallback = 'nota') {
|
|
339
|
-
|
|
364
|
+
export function slugify(text, fallback = 'nota', maxLen = 60) {
|
|
365
|
+
let slug = String(text || '')
|
|
340
366
|
.normalize('NFD')
|
|
341
367
|
.replace(/[\u0300-\u036f]/g, '')
|
|
342
368
|
.toLowerCase()
|
|
343
369
|
.replace(/[^a-z0-9]+/g, '-')
|
|
344
|
-
.replace(/^-+|-+$/g, '')
|
|
345
|
-
|
|
346
|
-
|
|
370
|
+
.replace(/^-+|-+$/g, '');
|
|
371
|
+
if (slug.length > maxLen) {
|
|
372
|
+
// Truncate on a word boundary (last '-' before maxLen) when a reasonable one exists,
|
|
373
|
+
// instead of cutting mid-word \u2014 keeps generated note names readable.
|
|
374
|
+
const cut = slug.slice(0, maxLen);
|
|
375
|
+
const lastDash = cut.lastIndexOf('-');
|
|
376
|
+
slug = (lastDash > maxLen * 0.5 ? cut.slice(0, lastDash) : cut).replace(/-+$/g, '');
|
|
377
|
+
}
|
|
347
378
|
return slug || fallback;
|
|
348
379
|
}
|
|
349
380
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
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/init.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
DOTCONTEXT_GITIGNORE,
|
|
23
23
|
} from './taxonomy.mjs';
|
|
24
24
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
25
|
+
import { seedVaultViews } from './vault-views.mjs';
|
|
25
26
|
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
26
27
|
import {
|
|
27
28
|
SNIPPET_NAME,
|
|
@@ -364,7 +365,13 @@ export async function runInit(argv) {
|
|
|
364
365
|
try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
365
366
|
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
366
367
|
}
|
|
367
|
-
|
|
368
|
+
// Generated Bases + Dashboard MOC (folder-filtered views over the taxonomy) — non-destructive.
|
|
369
|
+
let viewsNote = '';
|
|
370
|
+
try {
|
|
371
|
+
const views = seedVaultViews(vaultPath);
|
|
372
|
+
if (views.length) viewsNote = `, ${views.length} view(s) + dashboard`;
|
|
373
|
+
} catch { /* views são bônus — nunca derrubam o init */ }
|
|
374
|
+
log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded${viewsNote}`);
|
|
368
375
|
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
369
376
|
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
370
377
|
const synced = syncDefs(vaultPath, projectPath);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated Obsidian Bases + a Dashboard MOC — the vault's structural index (0.23.0).
|
|
2
|
+
// Each taxonomy folder gets a folder-filtered `.base` (NOT tag-filtered: a tag filter hid ~1/3
|
|
3
|
+
// of the corpus in production) and 00-Dashboard.md embeds them. Locale-aware; non-destructive
|
|
4
|
+
// (never overwrites a user's own base) unless force is passed. Zero runtime deps.
|
|
5
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
8
|
+
|
|
9
|
+
// Per-area view: which folder, which frontmatter columns, and their display labels.
|
|
10
|
+
function areaSpecs(loc) {
|
|
11
|
+
const f = loc.folders;
|
|
12
|
+
const en = loc.id === 'en';
|
|
13
|
+
const date = en ? 'Date' : 'Data';
|
|
14
|
+
return [
|
|
15
|
+
{ key: 'sessoes', folder: f.sessions, title: en ? 'Sessions' : 'Sessões',
|
|
16
|
+
cols: ['date', 'provider', 'custo_modelo_usd', 'prompts', 'tool_calls'],
|
|
17
|
+
labels: { date, provider: en ? 'Agent' : 'Agente', custo_modelo_usd: 'US$', prompts: 'Prompts', tool_calls: 'Tools' } },
|
|
18
|
+
{ key: 'decisoes', folder: f.decisions, title: en ? 'Decisions' : 'Decisões',
|
|
19
|
+
cols: ['date', 'status'], labels: { date, status: 'Status' } },
|
|
20
|
+
{ key: 'bugs', folder: f.bugs, title: 'Bugs',
|
|
21
|
+
cols: ['date', 'status', 'severity'], labels: { date, status: 'Status', severity: en ? 'Severity' : 'Severidade' } },
|
|
22
|
+
{ key: 'aprendizados', folder: f.learnings, title: en ? 'Learnings' : 'Aprendizados',
|
|
23
|
+
cols: ['date'], labels: { date } },
|
|
24
|
+
{ key: 'specs', folder: f.specs, title: 'Specs', cols: ['date'], labels: { date } },
|
|
25
|
+
{ key: 'mudancas', folder: f.changes, title: en ? 'Changes' : 'Mudanças',
|
|
26
|
+
cols: ['date', 'status'], labels: { date, status: 'Status' } },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A minimal, valid Base: folder filter + one table view. No formulas (that's where Base syntax
|
|
31
|
+
// errors hide) — the folder filter is the whole fix.
|
|
32
|
+
export function renderBase(spec) {
|
|
33
|
+
const props = spec.cols.map((c) => ` ${c}:\n displayName: ${spec.labels[c] || c}`).join('\n');
|
|
34
|
+
const order = ['file.name', ...spec.cols].map((c) => ` - ${c}`).join('\n');
|
|
35
|
+
return `filters:
|
|
36
|
+
and:
|
|
37
|
+
- file.inFolder("${spec.folder}")
|
|
38
|
+
- file.ext == "md"
|
|
39
|
+
properties:
|
|
40
|
+
${props}
|
|
41
|
+
views:
|
|
42
|
+
- type: table
|
|
43
|
+
name: ${spec.title}
|
|
44
|
+
order:
|
|
45
|
+
${order}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderDashboard(specs, en) {
|
|
50
|
+
const blocks = specs.map((s) => `## ${s.title}\n\n![[${s.key}.base]]`).join('\n\n---\n\n');
|
|
51
|
+
return `---
|
|
52
|
+
tags:
|
|
53
|
+
- dashboard
|
|
54
|
+
cssclasses:
|
|
55
|
+
- topic-dashboard
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# ${en ? 'Vault — Overview' : 'Vault — Visão Geral'}
|
|
59
|
+
|
|
60
|
+
> ${en ? 'Generated by wendkeep. Each section embeds a folder-filtered Base view (needs the Obsidian Bases core plugin).' : 'Gerado pelo wendkeep. Cada seção embeda uma view Base filtrada por pasta (requer o core plugin Bases do Obsidian).'}
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
${blocks}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Write one `<key>.base` per area + 00-Dashboard.md. Non-destructive unless force. Returns the
|
|
69
|
+
// list of files written.
|
|
70
|
+
export function seedVaultViews(vaultBase, { force = false } = {}) {
|
|
71
|
+
const loc = getLocale(vaultBase);
|
|
72
|
+
const specs = areaSpecs(loc);
|
|
73
|
+
const written = [];
|
|
74
|
+
for (const s of specs) {
|
|
75
|
+
const p = join(vaultBase, `${s.key}.base`);
|
|
76
|
+
if (force || !existsSync(p)) { writeFileSync(p, renderBase(s), 'utf8'); written.push(`${s.key}.base`); }
|
|
77
|
+
}
|
|
78
|
+
const dash = join(vaultBase, '00-Dashboard.md');
|
|
79
|
+
if (force || !existsSync(dash)) { writeFileSync(dash, renderDashboard(specs, loc.id === 'en'), 'utf8'); written.push('00-Dashboard.md'); }
|
|
80
|
+
return written;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function opt(argv, name) {
|
|
84
|
+
const i = argv.indexOf(name);
|
|
85
|
+
if (i >= 0) return argv[i + 1];
|
|
86
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
87
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// `wendkeep dashboard [--vault P] [--force]` — (re)generate the Bases + Dashboard.
|
|
91
|
+
export function runDashboard(argv) {
|
|
92
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
93
|
+
if (!vaultRaw) { process.stderr.write('wendkeep dashboard: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
94
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
95
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep dashboard: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
96
|
+
const written = seedVaultViews(vaultBase, { force: argv.includes('--force') });
|
|
97
|
+
process.stdout.write(written.length ? `dashboard: ${written.length} arquivo(s) — ${written.join(', ')}\n` : 'dashboard: nada a fazer (use --force para regenerar)\n');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|