skillwiki 0.2.1-beta.9 → 0.2.2
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/dist/chunk-TPS5XD2J.js +19 -0
- package/dist/cli.js +3594 -321
- package/dist/git-M4WGJ5G3.js +9 -0
- package/package.json +3 -1
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/package.json +2 -1
- package/skills/proj-decide/SKILL.md +2 -1
- package/skills/proj-distill/SKILL.md +8 -1
- package/skills/proj-init/SKILL.md +2 -1
- package/skills/proj-work/SKILL.md +2 -0
- package/skills/using-skillwiki/SKILL.md +12 -4
- package/skills/wiki-adapter-prd/SKILL.md +1 -0
- package/skills/wiki-add-task/SKILL.md +46 -31
- package/skills/wiki-archive/SKILL.md +5 -2
- package/skills/wiki-audit/SKILL.md +1 -0
- package/skills/wiki-canvas/SKILL.md +57 -0
- package/skills/wiki-crystallize/SKILL.md +2 -0
- package/skills/wiki-gate-plan-mode/SKILL.md +80 -0
- package/skills/wiki-ingest/SKILL.md +2 -0
- package/skills/wiki-init/SKILL.md +1 -0
- package/skills/wiki-lint/SKILL.md +1 -0
- package/skills/wiki-query/SKILL.md +1 -0
- package/skills/wiki-reingest/SKILL.md +1 -0
- package/skills/wiki-sync/SKILL.md +91 -0
- package/templates/index.md +3 -0
- package/templates/project-README.md +6 -0
package/dist/cli.js
CHANGED
|
@@ -2,43 +2,20 @@
|
|
|
2
2
|
import {
|
|
3
3
|
semverGt
|
|
4
4
|
} from "./chunk-XM5IYZX7.js";
|
|
5
|
+
import {
|
|
6
|
+
git,
|
|
7
|
+
gitStrict
|
|
8
|
+
} from "./chunk-TPS5XD2J.js";
|
|
5
9
|
|
|
6
10
|
// src/cli.ts
|
|
7
|
-
import { readFileSync as
|
|
11
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
12
|
+
import { join as join37 } from "path";
|
|
8
13
|
import { Command } from "commander";
|
|
9
14
|
|
|
10
|
-
// src/utils/output.ts
|
|
11
|
-
function printJson(r) {
|
|
12
|
-
process.stdout.write(JSON.stringify(r) + "\n");
|
|
13
|
-
}
|
|
14
|
-
function printHuman(r) {
|
|
15
|
-
if (r.ok) {
|
|
16
|
-
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
17
|
-
process.stdout.write(`${r.data.humanHint}
|
|
18
|
-
`);
|
|
19
|
-
} else {
|
|
20
|
-
process.stdout.write(`OK
|
|
21
|
-
${formatData(r.data)}
|
|
22
|
-
`);
|
|
23
|
-
}
|
|
24
|
-
} else {
|
|
25
|
-
process.stdout.write(`ERR ${r.error}
|
|
26
|
-
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function formatData(d) {
|
|
30
|
-
if (d == null) return "";
|
|
31
|
-
if (typeof d === "string") return d;
|
|
32
|
-
return JSON.stringify(d, null, 2);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/commands/hash.ts
|
|
36
|
-
import { readFile } from "fs/promises";
|
|
37
|
-
import { createHash } from "crypto";
|
|
38
|
-
|
|
39
15
|
// ../shared/src/exit-codes.ts
|
|
40
16
|
var ExitCode = {
|
|
41
17
|
OK: 0,
|
|
18
|
+
INTERNAL_ERROR: 1,
|
|
42
19
|
FILE_NOT_FOUND: 2,
|
|
43
20
|
MISSING_CLOSING_DELIMITER: 3,
|
|
44
21
|
SCHEME_REJECTED: 4,
|
|
@@ -73,7 +50,16 @@ var ExitCode = {
|
|
|
73
50
|
RAW_DEDUP_DETECTED: 33,
|
|
74
51
|
MIGRATION_APPLIED: 34,
|
|
75
52
|
UNKNOWN_WIKI_PROFILE: 35,
|
|
76
|
-
DEDUP_APPLIED: 36
|
|
53
|
+
DEDUP_APPLIED: 36,
|
|
54
|
+
PROJECT_NOT_FOUND: 37,
|
|
55
|
+
SYMLINK_FAILED: 38,
|
|
56
|
+
COMPOUND_PROMOTED: 39,
|
|
57
|
+
SKILL_VERSION_MISMATCH: 40,
|
|
58
|
+
INGEST_VALIDATION_FAILED: 41,
|
|
59
|
+
SYNC_PUSH_FAILED: 42,
|
|
60
|
+
SYNC_PULL_FAILED: 43,
|
|
61
|
+
BACKUP_SYNC_FAILED: 44,
|
|
62
|
+
BACKUP_RESTORE_CONFLICTS: 45
|
|
77
63
|
};
|
|
78
64
|
|
|
79
65
|
// ../shared/src/json-output.ts
|
|
@@ -97,7 +83,7 @@ var TypedKnowledgeSchema = z.object({
|
|
|
97
83
|
aliases: z.array(z.string()).optional(),
|
|
98
84
|
created: isoDate,
|
|
99
85
|
updated: isoDate,
|
|
100
|
-
type: z.enum(["entity", "concept", "comparison", "query"
|
|
86
|
+
type: z.enum(["entity", "concept", "comparison", "query"]),
|
|
101
87
|
tags: z.array(z.string()),
|
|
102
88
|
sources: z.array(z.string()).min(1),
|
|
103
89
|
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
@@ -115,17 +101,16 @@ var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
|
115
101
|
var RawSourceSchema = z.object({
|
|
116
102
|
title: z.string().min(1).optional(),
|
|
117
103
|
source_url: z.string().nullable(),
|
|
104
|
+
created: isoDate.optional(),
|
|
118
105
|
ingested: isoDate,
|
|
119
106
|
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
120
|
-
sha256: sha256Hex,
|
|
107
|
+
sha256: sha256Hex.optional(),
|
|
121
108
|
project: wikilink.optional(),
|
|
122
109
|
work_item: wikilink.optional(),
|
|
123
|
-
kind: z.enum(["postmortem", "session-log", "meeting-notes", "other"]).optional()
|
|
110
|
+
kind: z.enum(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]).optional()
|
|
124
111
|
}).superRefine((v, ctx) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (present !== 0 && present !== 3) {
|
|
128
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "project, work_item, kind must all be set together" });
|
|
112
|
+
if (v.work_item !== void 0 && (v.project === void 0 || v.kind === void 0)) {
|
|
113
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "project and kind are required when work_item is set" });
|
|
129
114
|
}
|
|
130
115
|
});
|
|
131
116
|
var WorkItemSchema = z.object({
|
|
@@ -162,20 +147,38 @@ var CompoundSchema = z.object({
|
|
|
162
147
|
promoted_to: wikilink.optional(),
|
|
163
148
|
cssclasses: z.array(z.string()).optional()
|
|
164
149
|
});
|
|
150
|
+
var MetaSchema = z.object({
|
|
151
|
+
title: z.string().min(1),
|
|
152
|
+
aliases: z.array(z.string()).optional(),
|
|
153
|
+
created: isoDate,
|
|
154
|
+
updated: isoDate,
|
|
155
|
+
type: z.literal("meta"),
|
|
156
|
+
tags: z.array(z.string()),
|
|
157
|
+
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
158
|
+
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
159
|
+
provenance_projects: z.array(wikilink).min(2, "meta pages must reference \u22652 projects")
|
|
160
|
+
}).superRefine((v, ctx) => {
|
|
161
|
+
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
162
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
165
|
function detectSchema(fm) {
|
|
166
166
|
const COMPOUND_TYPES = /* @__PURE__ */ new Set(["lesson", "pattern", "antipattern", "gotcha"]);
|
|
167
167
|
if (typeof fm.type === "string" && COMPOUND_TYPES.has(fm.type) && "project" in fm) return { schema: "compound" };
|
|
168
|
+
if (fm.type === "meta") return { schema: "meta" };
|
|
168
169
|
if ("type" in fm && "sources" in fm) return { schema: "typed-knowledge" };
|
|
169
|
-
if (
|
|
170
|
+
if ("ingested" in fm && ("source_url" in fm || "sha256" in fm)) return { schema: "raw" };
|
|
171
|
+
const RAW_KINDS = /* @__PURE__ */ new Set(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]);
|
|
172
|
+
if ("ingested" in fm && typeof fm.kind === "string" && RAW_KINDS.has(fm.kind)) return { schema: "raw" };
|
|
170
173
|
if ("kind" in fm && "status" in fm) return { schema: "work-item" };
|
|
171
174
|
return { schema: null };
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
// ../shared/src/blocked-hosts.ts
|
|
175
|
-
var METADATA_HOSTS = [
|
|
178
|
+
var METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
176
179
|
"metadata.google.internal",
|
|
177
180
|
"metadata"
|
|
178
|
-
];
|
|
181
|
+
]);
|
|
179
182
|
var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
|
|
180
183
|
function ipv4ToInt(ip) {
|
|
181
184
|
const parts = ip.split(".");
|
|
@@ -197,7 +200,7 @@ function inRange(ip, baseStr, prefix) {
|
|
|
197
200
|
}
|
|
198
201
|
function isBlockedHost(host) {
|
|
199
202
|
const lower = host.toLowerCase();
|
|
200
|
-
if (METADATA_HOSTS.
|
|
203
|
+
if (METADATA_HOSTS.has(lower)) return true;
|
|
201
204
|
if (METADATA_IPS.has(host)) return true;
|
|
202
205
|
if (lower === "::1") return true;
|
|
203
206
|
if (lower.startsWith("fe80:")) return true;
|
|
@@ -210,6 +213,61 @@ function isBlockedHost(host) {
|
|
|
210
213
|
return false;
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
// ../shared/src/error-message.ts
|
|
217
|
+
function getErrorMessage(e) {
|
|
218
|
+
return e instanceof Error ? e.message : String(e);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/utils/output.ts
|
|
222
|
+
function printJson(r) {
|
|
223
|
+
process.stdout.write(JSON.stringify(r) + "\n");
|
|
224
|
+
}
|
|
225
|
+
function printHuman(r) {
|
|
226
|
+
if (r.ok) {
|
|
227
|
+
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
228
|
+
process.stdout.write(`${r.data.humanHint}
|
|
229
|
+
`);
|
|
230
|
+
} else {
|
|
231
|
+
process.stdout.write(`OK
|
|
232
|
+
${formatData(r.data)}
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
process.stdout.write(`ERR ${r.error}
|
|
237
|
+
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function formatData(d) {
|
|
241
|
+
if (d == null) return "";
|
|
242
|
+
if (typeof d === "string") return d;
|
|
243
|
+
return JSON.stringify(d, null, 2);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/utils/deprecation.ts
|
|
247
|
+
import { readFileSync } from "fs";
|
|
248
|
+
import { join } from "path";
|
|
249
|
+
function getDeprecatedWarnings(home) {
|
|
250
|
+
const manifestPath = join(home, ".claude", "skills", "wiki-manifest.json");
|
|
251
|
+
try {
|
|
252
|
+
const raw = readFileSync(manifestPath, "utf8");
|
|
253
|
+
const manifest = JSON.parse(raw);
|
|
254
|
+
if (!manifest.skills) return [];
|
|
255
|
+
const warnings = [];
|
|
256
|
+
for (const [dirName, meta] of Object.entries(manifest.skills)) {
|
|
257
|
+
if (meta.deprecated) {
|
|
258
|
+
warnings.push(`\u26A0 Skill "${meta.name || dirName}" is deprecated. See SKILL.md for migration notes.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return warnings;
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/commands/hash.ts
|
|
268
|
+
import { readFile } from "fs/promises";
|
|
269
|
+
import { createHash } from "crypto";
|
|
270
|
+
|
|
213
271
|
// src/parsers/frontmatter.ts
|
|
214
272
|
import yaml from "js-yaml";
|
|
215
273
|
var FM_OPEN = /^---\r?\n/;
|
|
@@ -233,7 +291,7 @@ function extractFrontmatter(text) {
|
|
|
233
291
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
|
|
234
292
|
return ok(parsed);
|
|
235
293
|
} catch (e) {
|
|
236
|
-
return err("INVALID_FRONTMATTER", { message: e
|
|
294
|
+
return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
|
|
237
295
|
}
|
|
238
296
|
}
|
|
239
297
|
|
|
@@ -298,12 +356,22 @@ function sanitizeUrl(u) {
|
|
|
298
356
|
}
|
|
299
357
|
|
|
300
358
|
// src/commands/validate.ts
|
|
301
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
359
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
360
|
+
import { join as join2, resolve, relative, sep } from "path";
|
|
361
|
+
var TYPE_TO_SECTION = {
|
|
362
|
+
entity: "Entities",
|
|
363
|
+
concept: "Concepts",
|
|
364
|
+
comparison: "Comparisons",
|
|
365
|
+
query: "Queries",
|
|
366
|
+
summary: "Summaries",
|
|
367
|
+
meta: "Meta"
|
|
368
|
+
};
|
|
302
369
|
var SCHEMAS = {
|
|
303
370
|
"typed-knowledge": TypedKnowledgeSchema,
|
|
304
371
|
"raw": RawSourceSchema,
|
|
305
372
|
"work-item": WorkItemSchema,
|
|
306
|
-
"compound": CompoundSchema
|
|
373
|
+
"compound": CompoundSchema,
|
|
374
|
+
"meta": MetaSchema
|
|
307
375
|
};
|
|
308
376
|
async function runValidate(input) {
|
|
309
377
|
let text;
|
|
@@ -321,36 +389,120 @@ async function runValidate(input) {
|
|
|
321
389
|
}
|
|
322
390
|
const det = detectSchema(fm.data);
|
|
323
391
|
if (!det.schema) {
|
|
324
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
392
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], index_updated: false, log_updated: false, humanHint: "schema not detected" }) };
|
|
325
393
|
}
|
|
326
394
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
327
395
|
if (!parsed.success) {
|
|
328
396
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
329
397
|
return {
|
|
330
398
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
331
|
-
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
399
|
+
result: ok({ schema: det.schema, valid: false, errors, index_updated: false, log_updated: false, humanHint: `INVALID (${det.schema})
|
|
332
400
|
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
333
401
|
};
|
|
334
402
|
}
|
|
335
|
-
|
|
403
|
+
if (input.apply && !input.vault) {
|
|
404
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: "--vault is required when --apply is set" }) };
|
|
405
|
+
}
|
|
406
|
+
let indexUpdated = false;
|
|
407
|
+
let logUpdated = false;
|
|
408
|
+
let applyHint = "";
|
|
409
|
+
if (input.apply && input.vault) {
|
|
410
|
+
const absFile = resolve(input.file);
|
|
411
|
+
const absVault = resolve(input.vault);
|
|
412
|
+
const relPath = relative(absVault, absFile).split(sep).join("/");
|
|
413
|
+
if (relPath.startsWith("..")) {
|
|
414
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
|
|
415
|
+
}
|
|
416
|
+
const pageType = "type" in parsed.data && typeof parsed.data.type === "string" ? parsed.data.type : "";
|
|
417
|
+
const title = typeof parsed.data.title === "string" ? parsed.data.title : relPath.replace(/\.md$/, "");
|
|
418
|
+
if (det.schema === "typed-knowledge" || det.schema === "meta") {
|
|
419
|
+
indexUpdated = await addToIndex(input.vault, relPath, title, pageType);
|
|
420
|
+
}
|
|
421
|
+
logUpdated = await appendToLog(input.vault, relPath);
|
|
422
|
+
if (indexUpdated) applyHint += `
|
|
423
|
+
index: added [[${relPath.replace(/\.md$/, "")}]]`;
|
|
424
|
+
if (logUpdated) applyHint += "\n log: appended entry";
|
|
425
|
+
}
|
|
426
|
+
return { exitCode: ExitCode.OK, result: ok({
|
|
427
|
+
schema: det.schema,
|
|
428
|
+
valid: true,
|
|
429
|
+
errors: [],
|
|
430
|
+
index_updated: indexUpdated,
|
|
431
|
+
log_updated: logUpdated,
|
|
432
|
+
humanHint: `VALID (${det.schema})${applyHint}`
|
|
433
|
+
}) };
|
|
434
|
+
}
|
|
435
|
+
async function addToIndex(vault, relPath, title, pageType) {
|
|
436
|
+
const section = TYPE_TO_SECTION[pageType];
|
|
437
|
+
if (!section) return false;
|
|
438
|
+
const indexPath = join2(vault, "index.md");
|
|
439
|
+
let text;
|
|
440
|
+
try {
|
|
441
|
+
text = await readFile2(indexPath, "utf8");
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const ref = relPath.replace(/\.md$/, "");
|
|
446
|
+
if (text.includes(`[[${ref}]]`)) return false;
|
|
447
|
+
const entry = `- [[${ref}]] \u2014 ${title}`;
|
|
448
|
+
const lines = text.split("\n");
|
|
449
|
+
const sectionLine = `## ${section}`;
|
|
450
|
+
const sectionIdx = lines.findIndex((l) => l.trim() === sectionLine);
|
|
451
|
+
if (sectionIdx === -1) {
|
|
452
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
453
|
+
lines.push("", sectionLine, entry);
|
|
454
|
+
} else {
|
|
455
|
+
let endIdx = sectionIdx + 1;
|
|
456
|
+
while (endIdx < lines.length) {
|
|
457
|
+
if (lines[endIdx].startsWith("## ")) break;
|
|
458
|
+
endIdx++;
|
|
459
|
+
}
|
|
460
|
+
let insertAt = endIdx;
|
|
461
|
+
while (insertAt > sectionIdx + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
462
|
+
lines.splice(insertAt, 0, entry);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
await writeFile(indexPath, lines.join("\n"), "utf8");
|
|
466
|
+
} catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
async function appendToLog(vault, relPath) {
|
|
472
|
+
const logPath = join2(vault, "log.md");
|
|
473
|
+
let text;
|
|
474
|
+
try {
|
|
475
|
+
text = await readFile2(logPath, "utf8");
|
|
476
|
+
} catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
480
|
+
const entry = `
|
|
481
|
+
## [${today}] validate | added: ${relPath}`;
|
|
482
|
+
try {
|
|
483
|
+
await writeFile(logPath, text.trimEnd() + entry, "utf8");
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
336
488
|
}
|
|
337
489
|
|
|
338
490
|
// src/commands/graph.ts
|
|
339
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
491
|
+
import { writeFile as writeFile2, mkdir } from "fs/promises";
|
|
340
492
|
import { dirname } from "path";
|
|
341
493
|
|
|
342
494
|
// src/utils/vault.ts
|
|
343
495
|
import { readFile as readFile3, readdir, stat } from "fs/promises";
|
|
344
|
-
import { join, relative, sep } from "path";
|
|
345
|
-
var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries"];
|
|
496
|
+
import { join as join3, relative as relative2, sep as sep2 } from "path";
|
|
497
|
+
var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
346
498
|
async function scanVault(root) {
|
|
347
499
|
try {
|
|
348
|
-
await stat(
|
|
500
|
+
await stat(join3(root, "SCHEMA.md"));
|
|
349
501
|
} catch {
|
|
350
502
|
return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
|
|
351
503
|
}
|
|
352
504
|
const all = await walk(root);
|
|
353
|
-
const rels = all.map((p) => ({ absPath: p, relPath:
|
|
505
|
+
const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
|
|
354
506
|
return ok({
|
|
355
507
|
root,
|
|
356
508
|
typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
|
|
@@ -363,7 +515,7 @@ async function walk(dir) {
|
|
|
363
515
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
364
516
|
const out = [];
|
|
365
517
|
for (const e of entries) {
|
|
366
|
-
const p =
|
|
518
|
+
const p = join3(dir, e.name);
|
|
367
519
|
if (e.isDirectory()) out.push(...await walk(p));
|
|
368
520
|
else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
369
521
|
}
|
|
@@ -374,7 +526,7 @@ async function readPage(p) {
|
|
|
374
526
|
}
|
|
375
527
|
|
|
376
528
|
// src/parsers/wikilinks.ts
|
|
377
|
-
var FENCE =
|
|
529
|
+
var FENCE = /```[\s\S]*?```|`[^`\n]*`/g;
|
|
378
530
|
function extractBodyWikilinks(body) {
|
|
379
531
|
const stripped = body.replace(FENCE, "");
|
|
380
532
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -412,7 +564,7 @@ async function runGraphBuild(input) {
|
|
|
412
564
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
413
565
|
try {
|
|
414
566
|
await mkdir(dirname(input.out), { recursive: true });
|
|
415
|
-
await
|
|
567
|
+
await writeFile2(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
|
|
416
568
|
} catch (e) {
|
|
417
569
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
418
570
|
}
|
|
@@ -499,12 +651,21 @@ async function runOverlap(input) {
|
|
|
499
651
|
}
|
|
500
652
|
|
|
501
653
|
// src/utils/wiki-path.ts
|
|
502
|
-
import { join as
|
|
654
|
+
import { join as join4 } from "path";
|
|
503
655
|
|
|
504
656
|
// src/utils/dotenv.ts
|
|
505
|
-
import { readFile as readFile4, writeFile as
|
|
657
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
506
658
|
import { dirname as dirname2 } from "path";
|
|
507
|
-
var CONFIG_KEYS = [
|
|
659
|
+
var CONFIG_KEYS = [
|
|
660
|
+
"WIKI_PATH",
|
|
661
|
+
"WIKI_LANG",
|
|
662
|
+
"AUTO_COMMIT",
|
|
663
|
+
"BACKUP_ENDPOINT",
|
|
664
|
+
"BACKUP_BUCKET",
|
|
665
|
+
"BACKUP_REGION",
|
|
666
|
+
"BACKUP_ACCESS_KEY_ID",
|
|
667
|
+
"BACKUP_SECRET_ACCESS_KEY"
|
|
668
|
+
];
|
|
508
669
|
var _whitelist = new Set(CONFIG_KEYS);
|
|
509
670
|
var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
|
|
510
671
|
var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
|
|
@@ -543,7 +704,7 @@ async function parseDotenvFile(path) {
|
|
|
543
704
|
async function writeDotenv(filePath, entries, originalContent) {
|
|
544
705
|
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
545
706
|
await mkdir2(dirname2(filePath), { recursive: true });
|
|
546
|
-
await
|
|
707
|
+
await writeFile3(filePath, lines.join("\n") + "\n", "utf8");
|
|
547
708
|
}
|
|
548
709
|
function freshLines(entries) {
|
|
549
710
|
const out = [];
|
|
@@ -598,27 +759,27 @@ async function resolveInitTimePath(input) {
|
|
|
598
759
|
return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
|
|
599
760
|
}
|
|
600
761
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
601
|
-
const sw = await parseDotenvFile(
|
|
762
|
+
const sw = await parseDotenvFile(join4(input.home, ".skillwiki", ".env"));
|
|
602
763
|
if (sw.WIKI_PATH !== void 0) {
|
|
603
764
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
|
|
604
765
|
return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
|
|
605
766
|
}
|
|
606
767
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
607
|
-
const hermes = await parseDotenvFile(
|
|
768
|
+
const hermes = await parseDotenvFile(join4(input.home, ".hermes", ".env"));
|
|
608
769
|
if (hermes.WIKI_PATH !== void 0) {
|
|
609
770
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
|
|
610
771
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
611
772
|
}
|
|
612
773
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
613
774
|
if (input.cwd) {
|
|
614
|
-
const projCfg = await parseDotenvFile(
|
|
775
|
+
const projCfg = await parseDotenvFile(join4(input.cwd, ".skillwiki", ".env"));
|
|
615
776
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
616
777
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
617
778
|
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
618
779
|
}
|
|
619
780
|
}
|
|
620
781
|
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
621
|
-
const fallback =
|
|
782
|
+
const fallback = join4(input.home, "wiki");
|
|
622
783
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
623
784
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
624
785
|
}
|
|
@@ -629,7 +790,7 @@ async function resolveRuntimePath(input) {
|
|
|
629
790
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
630
791
|
}
|
|
631
792
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
632
|
-
const swGlobal = await parseDotenvFile(
|
|
793
|
+
const swGlobal = await parseDotenvFile(join4(input.home, ".skillwiki", ".env"));
|
|
633
794
|
const wikiName = input.wiki;
|
|
634
795
|
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
635
796
|
if (wikiName.toLowerCase() === "default") {
|
|
@@ -673,7 +834,7 @@ async function resolveRuntimePath(input) {
|
|
|
673
834
|
}
|
|
674
835
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
675
836
|
if (input.cwd) {
|
|
676
|
-
const projCfg = await parseDotenvFile(
|
|
837
|
+
const projCfg = await parseDotenvFile(join4(input.cwd, ".skillwiki", ".env"));
|
|
677
838
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
678
839
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
679
840
|
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
@@ -788,12 +949,13 @@ function simulateRemoval(adj, removed) {
|
|
|
788
949
|
|
|
789
950
|
// src/commands/audit.ts
|
|
790
951
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
791
|
-
import { dirname as dirname3, resolve, join as
|
|
952
|
+
import { dirname as dirname3, resolve as resolve2, join as join5 } from "path";
|
|
792
953
|
|
|
793
954
|
// src/parsers/citations.ts
|
|
794
955
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
795
|
-
var INLINE_CODE =
|
|
956
|
+
var INLINE_CODE = /``[^`\n]+``|`[^`\n]+`/g;
|
|
796
957
|
var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
|
|
958
|
+
var FRONTMATTER = /^---\n[\s\S]*?\n---\n?/;
|
|
797
959
|
function stripFences(body) {
|
|
798
960
|
return body.replace(FENCE2, "").replace(INLINE_CODE, "");
|
|
799
961
|
}
|
|
@@ -816,28 +978,33 @@ function isLegacyCitationStyle(body) {
|
|
|
816
978
|
const markers = extractCitationMarkers(body);
|
|
817
979
|
if (markers.length === 0) return false;
|
|
818
980
|
if (!hasSourcesFooter(body)) return true;
|
|
819
|
-
const lines = stripFences(body).split("\n");
|
|
981
|
+
const lines = stripFences(body.replace(FRONTMATTER, "")).split("\n");
|
|
820
982
|
let inSources = false;
|
|
983
|
+
let lastNonBlankWasTable = false;
|
|
821
984
|
for (const line of lines) {
|
|
822
985
|
if (/^## Sources\b/.test(line.trim())) {
|
|
823
986
|
inSources = true;
|
|
824
987
|
continue;
|
|
825
988
|
}
|
|
826
989
|
if (inSources) continue;
|
|
827
|
-
const
|
|
828
|
-
if (
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE, "").trim();
|
|
832
|
-
if (afterLast.length > 0) return true;
|
|
833
|
-
const beforeFirst = line.slice(0, line.indexOf("^[raw/")).trim();
|
|
834
|
-
if (beforeFirst.length > 0 && !/[.!?]\s*$/.test(beforeFirst)) return true;
|
|
990
|
+
const matches = [...line.matchAll(MARKER_RE)];
|
|
991
|
+
if (matches.length === 0) {
|
|
992
|
+
if (line.trim().length > 0) lastNonBlankWasTable = /^\|/.test(line.trim());
|
|
993
|
+
continue;
|
|
835
994
|
}
|
|
995
|
+
const markerOnly = line.replace(MARKER_RE, "").trim();
|
|
996
|
+
if (markerOnly.length === 0 && !lastNonBlankWasTable) return true;
|
|
997
|
+
lastNonBlankWasTable = false;
|
|
998
|
+
const lastMatch = matches[matches.length - 1];
|
|
999
|
+
const afterLast = line.slice(lastMatch.index + lastMatch[0].length).replace(MARKER_RE, "").trim();
|
|
1000
|
+
if (afterLast.length > 0) return true;
|
|
1001
|
+
const beforeFirst = line.slice(0, matches[0].index).trim();
|
|
1002
|
+
if (beforeFirst.length > 0 && !/[.!?]["'"]*\s*$/.test(beforeFirst)) return true;
|
|
836
1003
|
}
|
|
837
1004
|
return false;
|
|
838
1005
|
}
|
|
839
1006
|
function hasOrphanedCitations(body) {
|
|
840
|
-
const stripped = stripFences(body);
|
|
1007
|
+
const stripped = stripFences(body.replace(FRONTMATTER, ""));
|
|
841
1008
|
const lines = stripped.split("\n");
|
|
842
1009
|
let inSources = false;
|
|
843
1010
|
let sourcesEnded = false;
|
|
@@ -870,7 +1037,8 @@ function hasOrphanedCitations(body) {
|
|
|
870
1037
|
}
|
|
871
1038
|
if (sourcesStartLine === -1) return false;
|
|
872
1039
|
if (sourcesEnded) {
|
|
873
|
-
|
|
1040
|
+
const scanStart = Math.max(lastNonBlankInSources + 1, sourcesStartLine + 1);
|
|
1041
|
+
for (let i = scanStart; i < lines.length; i++) {
|
|
874
1042
|
if (/\^\[raw\//.test(lines[i])) {
|
|
875
1043
|
return true;
|
|
876
1044
|
}
|
|
@@ -895,12 +1063,12 @@ async function runAudit(input) {
|
|
|
895
1063
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
896
1064
|
const split = splitFrontmatter(text);
|
|
897
1065
|
const body = split.ok ? split.data.body : text;
|
|
898
|
-
const vault = await findVaultRoot(dirname3(
|
|
1066
|
+
const vault = await findVaultRoot(dirname3(resolve2(input.file)));
|
|
899
1067
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
900
1068
|
const markers = extractCitationMarkers(body);
|
|
901
1069
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
902
1070
|
try {
|
|
903
|
-
await stat2(
|
|
1071
|
+
await stat2(join5(vault, m.target));
|
|
904
1072
|
return { ...m, resolved: true };
|
|
905
1073
|
} catch {
|
|
906
1074
|
return { ...m, resolved: false };
|
|
@@ -911,7 +1079,7 @@ async function runAudit(input) {
|
|
|
911
1079
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
912
1080
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
913
1081
|
const broken = resolved.filter((m) => !m.resolved);
|
|
914
|
-
const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
|
|
1082
|
+
const footerMatch = body.match(/\r?\n## Sources\r?\n([\s\S]*)$/);
|
|
915
1083
|
let footer_consistency;
|
|
916
1084
|
if (footerMatch) {
|
|
917
1085
|
const footerTargets = /* @__PURE__ */ new Set();
|
|
@@ -945,7 +1113,7 @@ async function findVaultRoot(start) {
|
|
|
945
1113
|
let cur = start;
|
|
946
1114
|
for (let i = 0; i < 20; i++) {
|
|
947
1115
|
try {
|
|
948
|
-
await stat2(
|
|
1116
|
+
await stat2(join5(cur, "SCHEMA.md"));
|
|
949
1117
|
return cur;
|
|
950
1118
|
} catch {
|
|
951
1119
|
}
|
|
@@ -955,13 +1123,53 @@ async function findVaultRoot(start) {
|
|
|
955
1123
|
}
|
|
956
1124
|
return null;
|
|
957
1125
|
}
|
|
1126
|
+
function stripWikilink(s) {
|
|
1127
|
+
return s.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
1128
|
+
}
|
|
1129
|
+
async function validateCompoundReferences(vault) {
|
|
1130
|
+
const scan = await scanVault(vault);
|
|
1131
|
+
if (!scan.ok) return scan;
|
|
1132
|
+
const slugToPage = /* @__PURE__ */ new Map();
|
|
1133
|
+
const pathToPage = /* @__PURE__ */ new Map();
|
|
1134
|
+
for (const p of scan.data.workItems) {
|
|
1135
|
+
slugToPage.set(p.relPath.replace(/\.md$/, "").split("/").pop().toLowerCase(), p);
|
|
1136
|
+
pathToPage.set(p.relPath, p);
|
|
1137
|
+
}
|
|
1138
|
+
const findings = [];
|
|
1139
|
+
for (const cp of scan.data.compound) {
|
|
1140
|
+
const text = await readPage(cp);
|
|
1141
|
+
const fm = extractFrontmatter(text);
|
|
1142
|
+
if (!fm.ok) continue;
|
|
1143
|
+
const projectRaw = fm.data.project;
|
|
1144
|
+
const workItems = fm.data.work_items;
|
|
1145
|
+
if (!projectRaw || !workItems?.length) continue;
|
|
1146
|
+
const projSlug = stripWikilink(String(projectRaw));
|
|
1147
|
+
for (const wi of workItems) {
|
|
1148
|
+
const target = stripWikilink(wi);
|
|
1149
|
+
const withExt = target.endsWith(".md") ? target : target + ".md";
|
|
1150
|
+
const resolved = pathToPage.get(withExt) ?? slugToPage.get(target.split("/").pop().replace(/\.md$/, "").toLowerCase());
|
|
1151
|
+
if (!resolved) {
|
|
1152
|
+
findings.push({ compound: cp.relPath, work_item: wi, kind: "missing", detail: `no work item found for [[${target}]]` });
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const wiFm = extractFrontmatter(await readPage(resolved));
|
|
1156
|
+
if (wiFm.ok && wiFm.data.project) {
|
|
1157
|
+
const wiProj = stripWikilink(String(wiFm.data.project));
|
|
1158
|
+
if (wiProj !== projSlug) {
|
|
1159
|
+
findings.push({ compound: cp.relPath, work_item: wi, kind: "cross_project", detail: `compound project [[${projSlug}]] != work_item project [[${wiProj}]]` });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return ok(findings);
|
|
1165
|
+
}
|
|
958
1166
|
|
|
959
1167
|
// src/commands/install.ts
|
|
960
|
-
import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
961
|
-
import { join as
|
|
1168
|
+
import { readdir as readdir2, stat as stat4, symlink, unlink, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
|
|
1169
|
+
import { join as join6, resolve as resolve3, dirname as dirname5 } from "path";
|
|
962
1170
|
|
|
963
1171
|
// src/utils/install-fs.ts
|
|
964
|
-
import { copyFile, mkdir as mkdir3, rename, writeFile as
|
|
1172
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile4, stat as stat3 } from "fs/promises";
|
|
965
1173
|
import { dirname as dirname4 } from "path";
|
|
966
1174
|
async function atomicCopyWithBackup(src, dst) {
|
|
967
1175
|
await mkdir3(dirname4(dst), { recursive: true });
|
|
@@ -984,14 +1192,49 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
984
1192
|
async function writeManifest(path, m) {
|
|
985
1193
|
await mkdir3(dirname4(path), { recursive: true });
|
|
986
1194
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
987
|
-
await
|
|
1195
|
+
await writeFile4(path, JSON.stringify(enriched, null, 2));
|
|
988
1196
|
}
|
|
989
1197
|
|
|
990
1198
|
// src/commands/install.ts
|
|
1199
|
+
function parseSkillMeta(content) {
|
|
1200
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1201
|
+
const meta = { name: "" };
|
|
1202
|
+
if (!fmMatch) return meta;
|
|
1203
|
+
const fm = fmMatch[1];
|
|
1204
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
1205
|
+
if (nameMatch) meta.name = nameMatch[1].trim();
|
|
1206
|
+
const versionMatch = fm.match(/^version:\s*(.+)$/m);
|
|
1207
|
+
if (versionMatch) meta.version = versionMatch[1].trim();
|
|
1208
|
+
const depMatch = fm.match(/^deprecated:\s*(.+)$/m);
|
|
1209
|
+
if (depMatch && /^(true|yes)$/i.test(depMatch[1].trim())) meta.deprecated = true;
|
|
1210
|
+
return meta;
|
|
1211
|
+
}
|
|
1212
|
+
async function createSymlink(src, dst) {
|
|
1213
|
+
await mkdir4(dirname5(dst), { recursive: true });
|
|
1214
|
+
try {
|
|
1215
|
+
await unlink(dst);
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
await symlink(resolve3(src), dst);
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
return err("SYMLINK_FAILED", { message: String(e) });
|
|
1222
|
+
}
|
|
1223
|
+
return ok({ linked: true });
|
|
1224
|
+
}
|
|
991
1225
|
async function runInstall(input) {
|
|
992
1226
|
let entries;
|
|
993
1227
|
try {
|
|
994
|
-
|
|
1228
|
+
const dirs = (await readdir2(input.skillsRoot, { withFileTypes: true })).filter((d) => d.isDirectory());
|
|
1229
|
+
const withSkill = [];
|
|
1230
|
+
for (const d of dirs) {
|
|
1231
|
+
try {
|
|
1232
|
+
await stat4(join6(input.skillsRoot, d.name, "SKILL.md"));
|
|
1233
|
+
withSkill.push(d.name);
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
entries = withSkill;
|
|
995
1238
|
} catch (e) {
|
|
996
1239
|
return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { message: String(e) }) };
|
|
997
1240
|
}
|
|
@@ -1000,45 +1243,85 @@ async function runInstall(input) {
|
|
|
1000
1243
|
}
|
|
1001
1244
|
const installed = [];
|
|
1002
1245
|
const backed_up = [];
|
|
1246
|
+
const version_warnings = [];
|
|
1247
|
+
const skillMetas = {};
|
|
1003
1248
|
for (const name of entries) {
|
|
1004
|
-
const src =
|
|
1005
|
-
const dst =
|
|
1249
|
+
const src = join6(input.skillsRoot, name, "SKILL.md");
|
|
1250
|
+
const dst = join6(input.target, name, "SKILL.md");
|
|
1006
1251
|
try {
|
|
1007
1252
|
await stat4(src);
|
|
1008
1253
|
} catch {
|
|
1009
1254
|
return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { missing: src }) };
|
|
1010
1255
|
}
|
|
1256
|
+
try {
|
|
1257
|
+
const content = await readFile6(src, "utf8");
|
|
1258
|
+
const meta = parseSkillMeta(content);
|
|
1259
|
+
meta.name = meta.name || name;
|
|
1260
|
+
skillMetas[name] = meta;
|
|
1261
|
+
if (meta.deprecated) {
|
|
1262
|
+
version_warnings.push(`${name}: DEPRECATED \u2014 will be removed in a future release`);
|
|
1263
|
+
}
|
|
1264
|
+
if (!input.dryRun) {
|
|
1265
|
+
try {
|
|
1266
|
+
const existingContent = await readFile6(dst, "utf8");
|
|
1267
|
+
const existingMeta = parseSkillMeta(existingContent);
|
|
1268
|
+
if (existingMeta.version && meta.version && existingMeta.version !== meta.version) {
|
|
1269
|
+
version_warnings.push(`${name}: version changed ${existingMeta.version} \u2192 ${meta.version}`);
|
|
1270
|
+
}
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1011
1276
|
if (input.dryRun) {
|
|
1012
1277
|
installed.push(dst);
|
|
1013
1278
|
continue;
|
|
1014
1279
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1280
|
+
if (input.symlink) {
|
|
1281
|
+
const r = await createSymlink(src, dst);
|
|
1282
|
+
if (!r.ok) return { exitCode: ExitCode.SYMLINK_FAILED, result: r };
|
|
1283
|
+
installed.push(dst);
|
|
1284
|
+
} else {
|
|
1285
|
+
const r = await atomicCopyWithBackup(src, dst);
|
|
1286
|
+
if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
|
|
1287
|
+
installed.push(dst);
|
|
1288
|
+
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
1289
|
+
}
|
|
1019
1290
|
}
|
|
1020
|
-
const binSrc =
|
|
1291
|
+
const binSrc = join6(input.skillsRoot, "bin", "skillwiki");
|
|
1021
1292
|
try {
|
|
1022
1293
|
await stat4(binSrc);
|
|
1023
|
-
const binDst =
|
|
1294
|
+
const binDst = join6(input.target, "bin", "skillwiki");
|
|
1024
1295
|
if (!input.dryRun) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1296
|
+
if (input.symlink) {
|
|
1297
|
+
const r = await createSymlink(binSrc, binDst);
|
|
1298
|
+
if (!r.ok) return { exitCode: ExitCode.SYMLINK_FAILED, result: r };
|
|
1299
|
+
installed.push(binDst);
|
|
1300
|
+
} else {
|
|
1301
|
+
const r = await atomicCopyWithBackup(binSrc, binDst);
|
|
1302
|
+
if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
|
|
1303
|
+
installed.push(binDst);
|
|
1304
|
+
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
1305
|
+
}
|
|
1029
1306
|
} else {
|
|
1030
1307
|
installed.push(binDst);
|
|
1031
1308
|
}
|
|
1032
1309
|
} catch {
|
|
1033
1310
|
}
|
|
1034
|
-
const manifest_path =
|
|
1035
|
-
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
1311
|
+
const manifest_path = join6(input.target, "wiki-manifest.json");
|
|
1312
|
+
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up, symlink: input.symlink || void 0, skills: skillMetas });
|
|
1313
|
+
const mode = input.symlink ? "symlink (dev mode)" : "copy";
|
|
1036
1314
|
const hintLines = [
|
|
1037
|
-
`installed: ${installed.length}`,
|
|
1315
|
+
`installed: ${installed.length} (${mode})`,
|
|
1038
1316
|
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
1039
1317
|
`manifest: ${manifest_path}`
|
|
1040
1318
|
];
|
|
1041
|
-
|
|
1319
|
+
if (version_warnings.length > 0) {
|
|
1320
|
+
hintLines.push(`version warnings: ${version_warnings.length}`);
|
|
1321
|
+
for (const w of version_warnings) hintLines.push(` ${w}`);
|
|
1322
|
+
}
|
|
1323
|
+
const exitCode = version_warnings.length > 0 ? ExitCode.SKILL_VERSION_MISMATCH : ExitCode.OK;
|
|
1324
|
+
return { exitCode, result: ok({ installed, backed_up, manifest_path, version_warnings, humanHint: hintLines.join("\n") }) };
|
|
1042
1325
|
}
|
|
1043
1326
|
|
|
1044
1327
|
// src/commands/path.ts
|
|
@@ -1067,7 +1350,7 @@ async function runPath(input) {
|
|
|
1067
1350
|
}
|
|
1068
1351
|
|
|
1069
1352
|
// src/utils/lang.ts
|
|
1070
|
-
import { join as
|
|
1353
|
+
import { join as join7 } from "path";
|
|
1071
1354
|
var ALIASES = {
|
|
1072
1355
|
english: "en",
|
|
1073
1356
|
en: "en",
|
|
@@ -1090,7 +1373,7 @@ async function resolveLang(input) {
|
|
|
1090
1373
|
if (input.envValue !== void 0 && input.envValue.length > 0) {
|
|
1091
1374
|
return { value: input.envValue, source: "env", canonical: normalizeLang(input.envValue) };
|
|
1092
1375
|
}
|
|
1093
|
-
const dotenv = await parseDotenvFile(
|
|
1376
|
+
const dotenv = await parseDotenvFile(join7(input.home, ".skillwiki", ".env"));
|
|
1094
1377
|
if (dotenv.WIKI_LANG !== void 0) {
|
|
1095
1378
|
return { value: dotenv.WIKI_LANG, source: "skillwiki-dotenv", canonical: normalizeLang(dotenv.WIKI_LANG) };
|
|
1096
1379
|
}
|
|
@@ -1098,7 +1381,7 @@ async function resolveLang(input) {
|
|
|
1098
1381
|
}
|
|
1099
1382
|
|
|
1100
1383
|
// src/commands/lang.ts
|
|
1101
|
-
import { join as
|
|
1384
|
+
import { join as join8 } from "path";
|
|
1102
1385
|
async function runLang(input) {
|
|
1103
1386
|
const resolved = await resolveLang({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
1104
1387
|
let chain;
|
|
@@ -1107,7 +1390,7 @@ async function runLang(input) {
|
|
|
1107
1390
|
{ source: "flag", matched: input.flag !== void 0 && input.flag.length > 0, value: input.flag },
|
|
1108
1391
|
{ source: "env", matched: input.envValue !== void 0 && input.envValue.length > 0, value: input.envValue }
|
|
1109
1392
|
];
|
|
1110
|
-
const sw = await parseDotenvFile(
|
|
1393
|
+
const sw = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
|
|
1111
1394
|
chain.push({ source: "skillwiki-dotenv", matched: sw.WIKI_LANG !== void 0, value: sw.WIKI_LANG });
|
|
1112
1395
|
chain.push({ source: "default", matched: resolved.source === "default", value: "en" });
|
|
1113
1396
|
}
|
|
@@ -1124,8 +1407,8 @@ async function runLang(input) {
|
|
|
1124
1407
|
}
|
|
1125
1408
|
|
|
1126
1409
|
// src/commands/init.ts
|
|
1127
|
-
import { mkdir as
|
|
1128
|
-
import { join as
|
|
1410
|
+
import { mkdir as mkdir5, readFile as readFile7, readdir as readdir3, writeFile as writeFile5 } from "fs/promises";
|
|
1411
|
+
import { join as join10 } from "path";
|
|
1129
1412
|
|
|
1130
1413
|
// src/parsers/taxonomy.ts
|
|
1131
1414
|
import yaml2 from "js-yaml";
|
|
@@ -1137,7 +1420,7 @@ function extractTaxonomy(schemaText) {
|
|
|
1137
1420
|
try {
|
|
1138
1421
|
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1139
1422
|
} catch (e) {
|
|
1140
|
-
return err("INVALID_FRONTMATTER", { message: e
|
|
1423
|
+
return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
|
|
1141
1424
|
}
|
|
1142
1425
|
if (parsed === null || typeof parsed !== "object") {
|
|
1143
1426
|
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
@@ -1152,6 +1435,48 @@ function extractTaxonomy(schemaText) {
|
|
|
1152
1435
|
return ok(tax);
|
|
1153
1436
|
}
|
|
1154
1437
|
|
|
1438
|
+
// src/utils/last-op.ts
|
|
1439
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
1440
|
+
import { join as join9 } from "path";
|
|
1441
|
+
var LAST_OP_DIR = ".skillwiki";
|
|
1442
|
+
var LAST_OP_FILE = "last-op.json";
|
|
1443
|
+
function lastOpPath(vault) {
|
|
1444
|
+
return join9(vault, LAST_OP_DIR, LAST_OP_FILE);
|
|
1445
|
+
}
|
|
1446
|
+
function readLastOp(vault) {
|
|
1447
|
+
const p = lastOpPath(vault);
|
|
1448
|
+
if (!existsSync(p)) return [];
|
|
1449
|
+
try {
|
|
1450
|
+
const raw = readFileSync2(p, "utf8");
|
|
1451
|
+
const parsed = JSON.parse(raw);
|
|
1452
|
+
if (!Array.isArray(parsed)) {
|
|
1453
|
+
unlinkSync(p);
|
|
1454
|
+
return [];
|
|
1455
|
+
}
|
|
1456
|
+
return parsed;
|
|
1457
|
+
} catch {
|
|
1458
|
+
try {
|
|
1459
|
+
unlinkSync(p);
|
|
1460
|
+
} catch (_e) {
|
|
1461
|
+
}
|
|
1462
|
+
return [];
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function appendLastOp(vault, entry) {
|
|
1466
|
+
const existing = readLastOp(vault);
|
|
1467
|
+
existing.push(entry);
|
|
1468
|
+
const dir = join9(vault, LAST_OP_DIR);
|
|
1469
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1470
|
+
writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
|
|
1471
|
+
}
|
|
1472
|
+
function clearLastOp(vault) {
|
|
1473
|
+
const p = lastOpPath(vault);
|
|
1474
|
+
try {
|
|
1475
|
+
unlinkSync(p);
|
|
1476
|
+
} catch (_e) {
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1155
1480
|
// src/commands/init.ts
|
|
1156
1481
|
var DEFAULT_TAXONOMY = [
|
|
1157
1482
|
"research",
|
|
@@ -1176,9 +1501,12 @@ var VAULT_DIRS = [
|
|
|
1176
1501
|
"queries",
|
|
1177
1502
|
"meta",
|
|
1178
1503
|
"projects",
|
|
1179
|
-
".obsidian"
|
|
1504
|
+
".obsidian",
|
|
1505
|
+
"_Templates"
|
|
1180
1506
|
];
|
|
1181
1507
|
var ATTACHMENT_FOLDER = "raw/assets";
|
|
1508
|
+
var NEW_FILE_FOLDER = "raw/transcripts";
|
|
1509
|
+
var TEMPLATE_FOLDER = "_Templates";
|
|
1182
1510
|
function extractDomainFromSchema(text) {
|
|
1183
1511
|
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
1184
1512
|
if (!m) return "";
|
|
@@ -1191,13 +1519,13 @@ async function discoverTagsFromPages(target, knownSlugs) {
|
|
|
1191
1519
|
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
1192
1520
|
let entries;
|
|
1193
1521
|
try {
|
|
1194
|
-
entries = (await readdir3(
|
|
1522
|
+
entries = (await readdir3(join10(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
1195
1523
|
} catch {
|
|
1196
1524
|
continue;
|
|
1197
1525
|
}
|
|
1198
1526
|
for (const file of entries) {
|
|
1199
1527
|
try {
|
|
1200
|
-
const text = await
|
|
1528
|
+
const text = await readFile7(join10(target, dir, file), "utf8");
|
|
1201
1529
|
const fm = extractFrontmatter(text);
|
|
1202
1530
|
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
1203
1531
|
for (const t of fm.data.tags) {
|
|
@@ -1216,7 +1544,7 @@ async function runInit(input) {
|
|
|
1216
1544
|
const canonicalLang = langRes.canonical;
|
|
1217
1545
|
let oldSchemaText;
|
|
1218
1546
|
try {
|
|
1219
|
-
oldSchemaText = await
|
|
1547
|
+
oldSchemaText = await readFile7(join10(target, "SCHEMA.md"), "utf8");
|
|
1220
1548
|
} catch {
|
|
1221
1549
|
}
|
|
1222
1550
|
if (oldSchemaText && !input.force) {
|
|
@@ -1225,21 +1553,23 @@ async function runInit(input) {
|
|
|
1225
1553
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
1226
1554
|
};
|
|
1227
1555
|
}
|
|
1228
|
-
const envPath =
|
|
1556
|
+
const envPath = join10(input.home, ".skillwiki", ".env");
|
|
1229
1557
|
let existingEnvRaw = "";
|
|
1230
1558
|
try {
|
|
1231
|
-
existingEnvRaw = await
|
|
1559
|
+
existingEnvRaw = await readFile7(envPath, "utf8");
|
|
1232
1560
|
} catch {
|
|
1233
1561
|
}
|
|
1234
1562
|
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
1235
1563
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
1236
|
-
|
|
1564
|
+
const explicitTarget = !!input.flag;
|
|
1565
|
+
const skipConflictCheck = explicitTarget || !!input.noEnv;
|
|
1566
|
+
if (!input.profile && !skipConflictCheck && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
1237
1567
|
return {
|
|
1238
1568
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
1239
1569
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
|
|
1240
1570
|
};
|
|
1241
1571
|
}
|
|
1242
|
-
if (!input.profile && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
1572
|
+
if (!input.profile && !skipConflictCheck && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
1243
1573
|
return {
|
|
1244
1574
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
1245
1575
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
@@ -1247,9 +1577,9 @@ async function runInit(input) {
|
|
|
1247
1577
|
}
|
|
1248
1578
|
const created = [];
|
|
1249
1579
|
try {
|
|
1250
|
-
await
|
|
1580
|
+
await mkdir5(target, { recursive: true });
|
|
1251
1581
|
for (const d of VAULT_DIRS) {
|
|
1252
|
-
await
|
|
1582
|
+
await mkdir5(join10(target, d), { recursive: true });
|
|
1253
1583
|
created.push(d + "/");
|
|
1254
1584
|
}
|
|
1255
1585
|
} catch (e) {
|
|
@@ -1278,9 +1608,9 @@ async function runInit(input) {
|
|
|
1278
1608
|
const discovered_tags = discovered.length;
|
|
1279
1609
|
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
1280
1610
|
try {
|
|
1281
|
-
const schemaTpl = await
|
|
1611
|
+
const schemaTpl = await readFile7(join10(input.templates, "SCHEMA.md"), "utf8");
|
|
1282
1612
|
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
1283
|
-
await
|
|
1613
|
+
await writeFile5(join10(target, "SCHEMA.md"), schema, "utf8");
|
|
1284
1614
|
created.push("SCHEMA.md");
|
|
1285
1615
|
} catch (e) {
|
|
1286
1616
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
@@ -1288,7 +1618,7 @@ async function runInit(input) {
|
|
|
1288
1618
|
const preserved = [];
|
|
1289
1619
|
async function writeOrPreserve(fileName, render) {
|
|
1290
1620
|
try {
|
|
1291
|
-
const existing = await
|
|
1621
|
+
const existing = await readFile7(join10(target, fileName), "utf8");
|
|
1292
1622
|
if (existing.split("\n").length > 10) {
|
|
1293
1623
|
preserved.push(fileName);
|
|
1294
1624
|
return void 0;
|
|
@@ -1296,7 +1626,7 @@ async function runInit(input) {
|
|
|
1296
1626
|
} catch {
|
|
1297
1627
|
}
|
|
1298
1628
|
try {
|
|
1299
|
-
await
|
|
1629
|
+
await writeFile5(join10(target, fileName), await render(), "utf8");
|
|
1300
1630
|
created.push(fileName);
|
|
1301
1631
|
return void 0;
|
|
1302
1632
|
} catch (e) {
|
|
@@ -1304,20 +1634,38 @@ async function runInit(input) {
|
|
|
1304
1634
|
}
|
|
1305
1635
|
}
|
|
1306
1636
|
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1307
|
-
const tpl = await
|
|
1637
|
+
const tpl = await readFile7(join10(input.templates, "index.md"), "utf8");
|
|
1308
1638
|
return tpl.replace("{{INIT_DATE}}", today);
|
|
1309
1639
|
});
|
|
1310
1640
|
if (err1) return err1;
|
|
1311
1641
|
const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
|
|
1312
|
-
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER }, null, 2) + "\n";
|
|
1642
|
+
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
|
|
1313
1643
|
});
|
|
1314
1644
|
if (errObsidian) return errObsidian;
|
|
1645
|
+
const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
|
|
1646
|
+
return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
|
|
1647
|
+
});
|
|
1648
|
+
if (errTemplatesJson) return errTemplatesJson;
|
|
1649
|
+
const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
|
|
1650
|
+
return [
|
|
1651
|
+
"---",
|
|
1652
|
+
"source_url:",
|
|
1653
|
+
"created: {{date:YYYY-MM-DD}}",
|
|
1654
|
+
"ingested: # filled by ingest pipeline",
|
|
1655
|
+
"kind: # idea | bug | task | note | other",
|
|
1656
|
+
'project: # optional: "[[slug]]"',
|
|
1657
|
+
"---",
|
|
1658
|
+
"",
|
|
1659
|
+
""
|
|
1660
|
+
].join("\n");
|
|
1661
|
+
});
|
|
1662
|
+
if (errTemplate) return errTemplate;
|
|
1315
1663
|
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1316
|
-
const tpl = await
|
|
1664
|
+
const tpl = await readFile7(join10(input.templates, "log.md"), "utf8");
|
|
1317
1665
|
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1318
1666
|
});
|
|
1319
1667
|
if (err22) return err22;
|
|
1320
|
-
const skipEnv = !!input.noEnv;
|
|
1668
|
+
const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
|
|
1321
1669
|
let envWritten = "";
|
|
1322
1670
|
if (!skipEnv) {
|
|
1323
1671
|
try {
|
|
@@ -1345,6 +1693,14 @@ async function runInit(input) {
|
|
|
1345
1693
|
`discovered tags: ${discovered_tags}`,
|
|
1346
1694
|
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1347
1695
|
].join("\n");
|
|
1696
|
+
if (created.length > 0) {
|
|
1697
|
+
appendLastOp(target, {
|
|
1698
|
+
operation: "init",
|
|
1699
|
+
summary: `initialized vault: ${domain}`,
|
|
1700
|
+
files: created,
|
|
1701
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1348
1704
|
return {
|
|
1349
1705
|
exitCode: ExitCode.OK,
|
|
1350
1706
|
result: ok({
|
|
@@ -1358,7 +1714,8 @@ async function runInit(input) {
|
|
|
1358
1714
|
env_skipped: skipEnv,
|
|
1359
1715
|
imported_from_hermes: importedFromHermes,
|
|
1360
1716
|
discovered_tags,
|
|
1361
|
-
humanHint
|
|
1717
|
+
humanHint,
|
|
1718
|
+
templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
|
|
1362
1719
|
})
|
|
1363
1720
|
};
|
|
1364
1721
|
}
|
|
@@ -1401,12 +1758,12 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
|
|
|
1401
1758
|
}
|
|
1402
1759
|
|
|
1403
1760
|
// src/commands/tag-audit.ts
|
|
1404
|
-
import { readFile as
|
|
1405
|
-
import { join as
|
|
1761
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1762
|
+
import { join as join11 } from "path";
|
|
1406
1763
|
async function runTagAudit(input) {
|
|
1407
1764
|
const scan = await scanVault(input.vault);
|
|
1408
1765
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1409
|
-
const schemaText = await
|
|
1766
|
+
const schemaText = await readFile8(join11(input.vault, "SCHEMA.md"), "utf8");
|
|
1410
1767
|
const tax = extractTaxonomy(schemaText);
|
|
1411
1768
|
if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
|
|
1412
1769
|
const allowed = new Set(tax.data);
|
|
@@ -1430,14 +1787,14 @@ async function runTagAudit(input) {
|
|
|
1430
1787
|
}
|
|
1431
1788
|
|
|
1432
1789
|
// src/commands/index-check.ts
|
|
1433
|
-
import { readFile as
|
|
1434
|
-
import { join as
|
|
1790
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1791
|
+
import { join as join12 } from "path";
|
|
1435
1792
|
async function runIndexCheck(input) {
|
|
1436
1793
|
const scan = await scanVault(input.vault);
|
|
1437
1794
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1438
1795
|
let indexText = "";
|
|
1439
1796
|
try {
|
|
1440
|
-
indexText = await
|
|
1797
|
+
indexText = await readFile9(join12(input.vault, "index.md"), "utf8");
|
|
1441
1798
|
} catch {
|
|
1442
1799
|
}
|
|
1443
1800
|
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
@@ -1446,12 +1803,18 @@ async function runIndexCheck(input) {
|
|
|
1446
1803
|
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1447
1804
|
}
|
|
1448
1805
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1806
|
+
const requiredSlugs = /* @__PURE__ */ new Map();
|
|
1449
1807
|
for (const p of scan.data.typedKnowledge) {
|
|
1450
1808
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1451
1809
|
fileSlugs.set(slug, p.relPath);
|
|
1810
|
+
requiredSlugs.set(slug, p.relPath);
|
|
1811
|
+
}
|
|
1812
|
+
for (const p of scan.data.compound) {
|
|
1813
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1814
|
+
fileSlugs.set(slug, p.relPath);
|
|
1452
1815
|
}
|
|
1453
1816
|
const missing_from_index = [];
|
|
1454
|
-
for (const [slug, relPath] of
|
|
1817
|
+
for (const [slug, relPath] of requiredSlugs.entries()) {
|
|
1455
1818
|
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1456
1819
|
}
|
|
1457
1820
|
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
@@ -1470,44 +1833,176 @@ async function runIndexCheck(input) {
|
|
|
1470
1833
|
}
|
|
1471
1834
|
|
|
1472
1835
|
// src/commands/stale.ts
|
|
1473
|
-
import { readFile as
|
|
1474
|
-
import { join as
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
const db = Date.parse(b);
|
|
1478
|
-
return Math.round((db - da) / 864e5);
|
|
1836
|
+
import { readdir as readdir4, rename as rename2, mkdir as mkdir6, readFile as readFile10 } from "fs/promises";
|
|
1837
|
+
import { join as join13 } from "path";
|
|
1838
|
+
function daysSince(isoDate2) {
|
|
1839
|
+
return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
|
|
1479
1840
|
}
|
|
1480
1841
|
async function runStale(input) {
|
|
1481
1842
|
const scan = await scanVault(input.vault);
|
|
1482
1843
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1844
|
+
const staleTranscripts = [];
|
|
1845
|
+
const incompleteWorkItems = [];
|
|
1846
|
+
const archived = [];
|
|
1847
|
+
const workDirs = /* @__PURE__ */ new Map();
|
|
1848
|
+
const projectsDir = join13(input.vault, "projects");
|
|
1849
|
+
let projectSlugs = [];
|
|
1850
|
+
try {
|
|
1851
|
+
projectSlugs = (await readdir4(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
for (const slug of projectSlugs) {
|
|
1855
|
+
const workPath = join13(projectsDir, slug, "work");
|
|
1856
|
+
let entries;
|
|
1857
|
+
try {
|
|
1858
|
+
entries = await readdir4(workPath, { withFileTypes: true });
|
|
1859
|
+
} catch {
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
for (const e of entries) {
|
|
1863
|
+
if (!e.isDirectory()) continue;
|
|
1864
|
+
const relDir = `projects/${slug}/work/${e.name}`;
|
|
1865
|
+
const absDir = join13(workPath, e.name);
|
|
1866
|
+
let status = "";
|
|
1867
|
+
let files;
|
|
1493
1868
|
try {
|
|
1494
|
-
|
|
1869
|
+
files = await readdir4(absDir);
|
|
1495
1870
|
} catch {
|
|
1871
|
+
workDirs.set(relDir, "");
|
|
1496
1872
|
continue;
|
|
1497
1873
|
}
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1874
|
+
for (const f of files) {
|
|
1875
|
+
if (!f.endsWith(".md")) continue;
|
|
1876
|
+
try {
|
|
1877
|
+
const fm = extractFrontmatter(await readFile10(join13(absDir, f), "utf8"));
|
|
1878
|
+
if (fm.ok && typeof fm.data.status === "string") {
|
|
1879
|
+
status = fm.data.status;
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
workDirs.set(relDir, status);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
const transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
|
|
1889
|
+
for (const t of transcripts) {
|
|
1890
|
+
const datePrefix = t.relPath.split("/").pop().slice(0, 10);
|
|
1891
|
+
for (const [dir, status] of workDirs) {
|
|
1892
|
+
if (dir.split("/").pop().startsWith(datePrefix) && (status === "done" || status === "invalid")) {
|
|
1893
|
+
staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const doneWorkItems = [];
|
|
1899
|
+
for (const [relDir, status] of workDirs) {
|
|
1900
|
+
const dirName = relDir.split("/").pop();
|
|
1901
|
+
const dateStr = dirName.slice(0, 10);
|
|
1902
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) continue;
|
|
1903
|
+
if (daysSince(dateStr) < input.days) continue;
|
|
1904
|
+
let files;
|
|
1905
|
+
try {
|
|
1906
|
+
files = await readdir4(join13(input.vault, relDir));
|
|
1907
|
+
} catch {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
const hasSpec = files.includes("spec.md"), hasPlan = files.includes("plan.md"), hasWI = files.includes("work-item.md");
|
|
1911
|
+
if (status === "done") {
|
|
1912
|
+
doneWorkItems.push({ path: relDir, reason: "completed \u2014 should be archived" });
|
|
1913
|
+
} else if (status === "invalid") {
|
|
1914
|
+
doneWorkItems.push({ path: relDir, reason: "invalid \u2014 should be archived" });
|
|
1915
|
+
} else if (hasSpec && !hasPlan) {
|
|
1916
|
+
incompleteWorkItems.push({ path: relDir, reason: "has spec but no plan" });
|
|
1917
|
+
} else if (hasWI && !hasSpec && !hasPlan) {
|
|
1918
|
+
incompleteWorkItems.push({ path: relDir, reason: "only work-item.md, no spec or plan" });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
const stale = [];
|
|
1922
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1923
|
+
try {
|
|
1924
|
+
const text = await readFile10(join13(input.vault, page.relPath), "utf8");
|
|
1925
|
+
const fm = extractFrontmatter(text);
|
|
1926
|
+
if (fm.ok && typeof fm.data.updated === "string") {
|
|
1927
|
+
const age = daysSince(fm.data.updated);
|
|
1928
|
+
if (age >= input.days) {
|
|
1929
|
+
stale.push({ page: page.relPath, reason: `updated ${age} days ago (threshold: ${input.days})` });
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (input.archive) {
|
|
1936
|
+
const archiveDir = join13(input.vault, "_archive", (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
1937
|
+
await mkdir6(archiveDir, { recursive: true });
|
|
1938
|
+
const citedRawPaths = /* @__PURE__ */ new Set();
|
|
1939
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1940
|
+
const text = await readFile10(join13(input.vault, page.relPath), "utf8").catch(() => "");
|
|
1941
|
+
for (const line of text.split("\n")) {
|
|
1942
|
+
for (const m of line.matchAll(/\^\[(raw\/[^\]]+)\]/g)) {
|
|
1943
|
+
citedRawPaths.add(m[1]);
|
|
1944
|
+
}
|
|
1945
|
+
for (const m of line.matchAll(/raw\/[^\s,\]"]+\.md/g)) {
|
|
1946
|
+
citedRawPaths.add(m[0]);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
for (const t of staleTranscripts) {
|
|
1951
|
+
if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
|
|
1952
|
+
const dest = join13(archiveDir, t.path.split("/").pop());
|
|
1953
|
+
try {
|
|
1954
|
+
await rename2(join13(input.vault, t.path), dest);
|
|
1955
|
+
archived.push(t.path);
|
|
1956
|
+
} catch {
|
|
1957
|
+
}
|
|
1502
1958
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1959
|
+
for (const w of [...incompleteWorkItems, ...doneWorkItems]) {
|
|
1960
|
+
const parts = w.path.split("/");
|
|
1961
|
+
if (parts.length >= 4 && parts[0] === "projects") {
|
|
1962
|
+
const slug = parts[1];
|
|
1963
|
+
const itemName = parts[3];
|
|
1964
|
+
const histDir = join13(input.vault, "projects", slug, "history", "archived-work");
|
|
1965
|
+
await mkdir6(histDir, { recursive: true });
|
|
1966
|
+
const dest = join13(histDir, itemName);
|
|
1967
|
+
try {
|
|
1968
|
+
await rename2(join13(input.vault, w.path), dest);
|
|
1969
|
+
archived.push(w.path);
|
|
1970
|
+
} catch {
|
|
1971
|
+
}
|
|
1972
|
+
} else {
|
|
1973
|
+
const dest = join13(archiveDir, w.path.replace(/\//g, "_"));
|
|
1974
|
+
try {
|
|
1975
|
+
await rename2(join13(input.vault, w.path), dest);
|
|
1976
|
+
archived.push(w.path);
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1507
1980
|
}
|
|
1508
1981
|
}
|
|
1509
|
-
if (
|
|
1510
|
-
|
|
1982
|
+
if (input.archive && archived.length > 0) {
|
|
1983
|
+
appendLastOp(input.vault, {
|
|
1984
|
+
operation: "stale-archive",
|
|
1985
|
+
summary: `archived ${archived.length} stale items`,
|
|
1986
|
+
files: archived,
|
|
1987
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
const total = stale.length + staleTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
|
|
1991
|
+
const hintLines = [];
|
|
1992
|
+
if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
|
|
1993
|
+
if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
|
|
1994
|
+
if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
1995
|
+
if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
1996
|
+
if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
|
|
1997
|
+
if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
|
|
1998
|
+
return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
|
|
1999
|
+
stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...incompleteWorkItems.map((w) => ({ page: w.path, reason: w.reason })), ...doneWorkItems.map((w) => ({ page: w.path, reason: w.reason }))],
|
|
2000
|
+
stale_transcripts: staleTranscripts,
|
|
2001
|
+
incomplete_work_items: incompleteWorkItems,
|
|
2002
|
+
done_work_items: doneWorkItems,
|
|
2003
|
+
archived,
|
|
2004
|
+
humanHint: hintLines.join("\n")
|
|
2005
|
+
}) };
|
|
1511
2006
|
}
|
|
1512
2007
|
|
|
1513
2008
|
// src/commands/pagesize.ts
|
|
@@ -1527,19 +2022,19 @@ async function runPagesize(input) {
|
|
|
1527
2022
|
}
|
|
1528
2023
|
|
|
1529
2024
|
// src/commands/log-rotate.ts
|
|
1530
|
-
import { readFile as
|
|
1531
|
-
import { join as
|
|
2025
|
+
import { readFile as readFile11, rename as rename3, writeFile as writeFile6, stat as stat5 } from "fs/promises";
|
|
2026
|
+
import { join as join14 } from "path";
|
|
1532
2027
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1533
2028
|
async function runLogRotate(input) {
|
|
1534
2029
|
try {
|
|
1535
|
-
await stat5(
|
|
2030
|
+
await stat5(join14(input.vault, "SCHEMA.md"));
|
|
1536
2031
|
} catch {
|
|
1537
2032
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1538
2033
|
}
|
|
1539
|
-
const logPath =
|
|
2034
|
+
const logPath = join14(input.vault, "log.md");
|
|
1540
2035
|
let logText;
|
|
1541
2036
|
try {
|
|
1542
|
-
logText = await
|
|
2037
|
+
logText = await readFile11(logPath, "utf8");
|
|
1543
2038
|
} catch {
|
|
1544
2039
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
1545
2040
|
}
|
|
@@ -1556,9 +2051,9 @@ async function runLogRotate(input) {
|
|
|
1556
2051
|
}
|
|
1557
2052
|
const newestYear = matches[matches.length - 1][1];
|
|
1558
2053
|
const rotatedName = `log-${newestYear}.md`;
|
|
1559
|
-
const rotatedPath =
|
|
2054
|
+
const rotatedPath = join14(input.vault, rotatedName);
|
|
1560
2055
|
try {
|
|
1561
|
-
await
|
|
2056
|
+
await rename3(logPath, rotatedPath);
|
|
1562
2057
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1563
2058
|
const fresh = `# Vault Log
|
|
1564
2059
|
|
|
@@ -1568,13 +2063,24 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1568
2063
|
|
|
1569
2064
|
- Previous log moved to ${rotatedName}
|
|
1570
2065
|
`;
|
|
1571
|
-
await
|
|
2066
|
+
await writeFile6(logPath, fresh, "utf8");
|
|
1572
2067
|
} catch (e) {
|
|
1573
2068
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1574
2069
|
}
|
|
2070
|
+
appendLastOp(input.vault, {
|
|
2071
|
+
operation: "log-rotate",
|
|
2072
|
+
summary: `rotated ${entries} entries to ${rotatedName}`,
|
|
2073
|
+
files: ["log.md", rotatedName],
|
|
2074
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2075
|
+
});
|
|
1575
2076
|
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1576
2077
|
}
|
|
1577
2078
|
|
|
2079
|
+
// src/commands/lint.ts
|
|
2080
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2081
|
+
import { readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
|
|
2082
|
+
import { join as join17 } from "path";
|
|
2083
|
+
|
|
1578
2084
|
// src/commands/topic-map-check.ts
|
|
1579
2085
|
var DEFAULT_THRESHOLD = 200;
|
|
1580
2086
|
async function runTopicMapCheck(input) {
|
|
@@ -1595,13 +2101,13 @@ async function runTopicMapCheck(input) {
|
|
|
1595
2101
|
}
|
|
1596
2102
|
|
|
1597
2103
|
// src/commands/index-link-format.ts
|
|
1598
|
-
import { readFile as
|
|
1599
|
-
import { join as
|
|
2104
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
2105
|
+
import { join as join15 } from "path";
|
|
1600
2106
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1601
2107
|
async function runIndexLinkFormat(input) {
|
|
1602
2108
|
let text = "";
|
|
1603
2109
|
try {
|
|
1604
|
-
text = await
|
|
2110
|
+
text = await readFile12(join15(input.vault, "index.md"), "utf8");
|
|
1605
2111
|
} catch {
|
|
1606
2112
|
}
|
|
1607
2113
|
const markdown_links = [];
|
|
@@ -1614,8 +2120,8 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
1614
2120
|
}
|
|
1615
2121
|
|
|
1616
2122
|
// src/commands/dedup.ts
|
|
1617
|
-
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
1618
|
-
import { join as
|
|
2123
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
2124
|
+
import { join as join16 } from "path";
|
|
1619
2125
|
async function runDedup(input) {
|
|
1620
2126
|
const scan = await scanVault(input.vault);
|
|
1621
2127
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1643,7 +2149,7 @@ async function runDedup(input) {
|
|
|
1643
2149
|
}
|
|
1644
2150
|
}
|
|
1645
2151
|
for (const page of scan.data.typedKnowledge) {
|
|
1646
|
-
const text =
|
|
2152
|
+
const text = readFileSync3(join16(input.vault, page.relPath), "utf-8");
|
|
1647
2153
|
let updated = text;
|
|
1648
2154
|
let changed = false;
|
|
1649
2155
|
for (const [oldPath, newPath] of replacements) {
|
|
@@ -1661,19 +2167,27 @@ async function runDedup(input) {
|
|
|
1661
2167
|
}
|
|
1662
2168
|
}
|
|
1663
2169
|
if (changed) {
|
|
1664
|
-
|
|
2170
|
+
writeFileSync2(join16(input.vault, page.relPath), updated);
|
|
1665
2171
|
rewired.push(page.relPath);
|
|
1666
2172
|
}
|
|
1667
2173
|
}
|
|
1668
2174
|
for (const [oldPath] of replacements) {
|
|
1669
|
-
const fullPath =
|
|
2175
|
+
const fullPath = join16(input.vault, oldPath);
|
|
1670
2176
|
try {
|
|
1671
|
-
|
|
2177
|
+
unlinkSync2(fullPath);
|
|
1672
2178
|
removed.push(oldPath);
|
|
1673
2179
|
} catch {
|
|
1674
2180
|
}
|
|
1675
2181
|
}
|
|
1676
2182
|
}
|
|
2183
|
+
if (input.apply && (rewired.length > 0 || removed.length > 0)) {
|
|
2184
|
+
appendLastOp(input.vault, {
|
|
2185
|
+
operation: "dedup",
|
|
2186
|
+
summary: `rewired ${rewired.length} pages, removed ${removed.length} duplicates`,
|
|
2187
|
+
files: [...rewired, ...removed],
|
|
2188
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
1677
2191
|
const exitCode = duplicates.length > 0 ? input.apply ? ExitCode.DEDUP_APPLIED : ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1678
2192
|
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1679
2193
|
if (duplicates.length > 0) {
|
|
@@ -1706,11 +2220,30 @@ function hasDuplicateFrontmatter(body) {
|
|
|
1706
2220
|
}
|
|
1707
2221
|
return false;
|
|
1708
2222
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2223
|
+
function extractSourceEntries(rawFm) {
|
|
2224
|
+
const lines = rawFm.split(/\r?\n/);
|
|
2225
|
+
const sourcesLineIdx = lines.findIndex((l) => /^sources:/.test(l));
|
|
2226
|
+
if (sourcesLineIdx === -1) return [];
|
|
2227
|
+
const sourcesLine = lines[sourcesLineIdx].trim();
|
|
2228
|
+
const inlineMatch = sourcesLine.match(/^sources:\s*\[(.+)]\s*$/);
|
|
2229
|
+
if (inlineMatch) {
|
|
2230
|
+
return [...inlineMatch[1].matchAll(/"[^"]*"|'[^']*'|[^,\s]\S*/g)].map((m) => m[0].replace(/,\s*$/, ""));
|
|
2231
|
+
}
|
|
2232
|
+
const entries = [];
|
|
2233
|
+
for (let i = sourcesLineIdx + 1; i < lines.length; i++) {
|
|
2234
|
+
const line = lines[i];
|
|
2235
|
+
if (!/^\s+- /.test(line)) break;
|
|
2236
|
+
entries.push(line.replace(/^\s+- /, "").trim());
|
|
2237
|
+
}
|
|
2238
|
+
return entries;
|
|
2239
|
+
}
|
|
2240
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
|
|
2241
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview"];
|
|
2242
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation"];
|
|
2243
|
+
async function runLint(input) {
|
|
1713
2244
|
const buckets = {};
|
|
2245
|
+
const fixed = [];
|
|
2246
|
+
const unresolved = [];
|
|
1714
2247
|
const links = await runLinks({ vault: input.vault });
|
|
1715
2248
|
if (links.result.ok && links.result.data.broken.length > 0) buckets.broken_wikilinks = links.result.data.broken;
|
|
1716
2249
|
if (!links.result.ok && links.result.error === "INVALID_FRONTMATTER") {
|
|
@@ -1732,8 +2265,12 @@ async function runLint(input) {
|
|
|
1732
2265
|
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1733
2266
|
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1734
2267
|
}
|
|
1735
|
-
const
|
|
1736
|
-
if (
|
|
2268
|
+
const staleResult = await runStale({ vault: input.vault, days: input.days });
|
|
2269
|
+
if (staleResult.result.ok) {
|
|
2270
|
+
const st = staleResult.result.data;
|
|
2271
|
+
const staleList = [...st.stale_transcripts.map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
|
|
2272
|
+
if (staleList.length > 0) buckets.stale_page = staleList;
|
|
2273
|
+
}
|
|
1737
2274
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
1738
2275
|
if (pagesize.result.ok && pagesize.result.data.oversized.length > 0) buckets.page_too_large = pagesize.result.data.oversized;
|
|
1739
2276
|
const rotate = await runLogRotate({ vault: input.vault, threshold: input.logThreshold, apply: false });
|
|
@@ -1751,6 +2288,8 @@ async function runLint(input) {
|
|
|
1751
2288
|
}
|
|
1752
2289
|
const dedup = await runDedup({ vault: input.vault });
|
|
1753
2290
|
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
2291
|
+
const compoundRefs = await validateCompoundReferences(input.vault);
|
|
2292
|
+
if (compoundRefs.ok && compoundRefs.data.length > 0) buckets.compound_refs = compoundRefs.data;
|
|
1754
2293
|
const scan = await scanVault(input.vault);
|
|
1755
2294
|
const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
|
|
1756
2295
|
const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
|
|
@@ -1762,6 +2301,7 @@ async function runLint(input) {
|
|
|
1762
2301
|
const noOverview = [];
|
|
1763
2302
|
const fmWikilinkFlags = [];
|
|
1764
2303
|
const wikilinkCitationFlags = [];
|
|
2304
|
+
const brokenSourceFlags = [];
|
|
1765
2305
|
for (const page of scan.data.typedKnowledge) {
|
|
1766
2306
|
const text = await readPage(page);
|
|
1767
2307
|
const split = splitFrontmatter(text);
|
|
@@ -1772,6 +2312,15 @@ async function runLint(input) {
|
|
|
1772
2312
|
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1773
2313
|
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1774
2314
|
if (hasWikilinkCitations(body)) wikilinkCitationFlags.push(page.relPath);
|
|
2315
|
+
const sourcesEntries = extractSourceEntries(rawFm);
|
|
2316
|
+
for (const entry of sourcesEntries) {
|
|
2317
|
+
let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
2318
|
+
rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
2319
|
+
if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
|
|
2320
|
+
if (!existsSync2(join17(input.vault, rawPath)) && !existsSync2(join17(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync2(join17(input.vault, "_archive", rawPath)) && !existsSync2(join17(input.vault, "_archive", rawPath + ".md"))) {
|
|
2321
|
+
brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
1775
2324
|
const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
|
|
1776
2325
|
for (const link of fmLinks) {
|
|
1777
2326
|
const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
@@ -1801,6 +2350,281 @@ async function runLint(input) {
|
|
|
1801
2350
|
if (noOverview.length > 0) buckets.missing_overview = noOverview;
|
|
1802
2351
|
if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
|
|
1803
2352
|
if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
|
|
2353
|
+
if (brokenSourceFlags.length > 0) buckets.broken_sources = brokenSourceFlags;
|
|
2354
|
+
const workItemHealth = [];
|
|
2355
|
+
const workItemDirs = /* @__PURE__ */ new Map();
|
|
2356
|
+
for (const page of scan.data.workItems) {
|
|
2357
|
+
const dir = page.relPath.replace(/\/(spec|plan|log)\.md$/, "");
|
|
2358
|
+
const pages = workItemDirs.get(dir) ?? [];
|
|
2359
|
+
pages.push(page);
|
|
2360
|
+
workItemDirs.set(dir, pages);
|
|
2361
|
+
}
|
|
2362
|
+
for (const [dir, pages] of workItemDirs) {
|
|
2363
|
+
const hasSpec = pages.some((p) => p.relPath.endsWith("/spec.md"));
|
|
2364
|
+
const hasPlan = pages.some((p) => p.relPath.endsWith("/plan.md"));
|
|
2365
|
+
if (hasSpec && !hasPlan) {
|
|
2366
|
+
const lastSegment = dir.split("/").pop();
|
|
2367
|
+
const dateMatch = lastSegment.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
2368
|
+
if (dateMatch) {
|
|
2369
|
+
const dirDate = Date.parse(dateMatch[1]);
|
|
2370
|
+
if (!isNaN(dirDate) && Date.now() - dirDate > 24 * 60 * 60 * 1e3) {
|
|
2371
|
+
workItemHealth.push(`${dir}/spec.md: has spec but no plan after 24h`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
for (const page of pages) {
|
|
2376
|
+
if (!page.relPath.endsWith("/spec.md")) continue;
|
|
2377
|
+
const text = await readPage(page);
|
|
2378
|
+
const fm = extractFrontmatter(text);
|
|
2379
|
+
if (fm.ok && fm.data.status === "in-progress" && !fm.data.started) {
|
|
2380
|
+
workItemHealth.push(`${page.relPath}: in-progress without started date`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (workItemHealth.length > 0) buckets.work_item_health = workItemHealth;
|
|
2385
|
+
const orphanedProjectPages = [];
|
|
2386
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2387
|
+
const text = await readPage(page);
|
|
2388
|
+
const fm = extractFrontmatter(text);
|
|
2389
|
+
if (!fm.ok) continue;
|
|
2390
|
+
const pp = fm.data.provenance_projects;
|
|
2391
|
+
if (!Array.isArray(pp)) continue;
|
|
2392
|
+
for (const entry of pp) {
|
|
2393
|
+
const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
|
|
2394
|
+
if (!slugMatch) continue;
|
|
2395
|
+
const slug = slugMatch[1];
|
|
2396
|
+
const knowledgePath = join17(input.vault, "projects", slug, "knowledge.md");
|
|
2397
|
+
if (!existsSync2(knowledgePath)) continue;
|
|
2398
|
+
const pageRef = page.relPath.replace(/\.md$/, "");
|
|
2399
|
+
try {
|
|
2400
|
+
const knowledgeContent = await readFile13(knowledgePath, "utf8");
|
|
2401
|
+
if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
|
|
2402
|
+
orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
|
|
2403
|
+
}
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
if (orphanedProjectPages.length > 0) buckets.orphaned_project_pages = orphanedProjectPages;
|
|
2409
|
+
if (input.fix && legacyPages.length > 0) {
|
|
2410
|
+
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
2411
|
+
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
2412
|
+
for (const relPath of legacyPages) {
|
|
2413
|
+
try {
|
|
2414
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
2415
|
+
const raw = await readFile13(absPath, "utf8");
|
|
2416
|
+
const split = splitFrontmatter(raw);
|
|
2417
|
+
if (!split.ok) {
|
|
2418
|
+
unresolved.push(relPath);
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
const body = split.data.body;
|
|
2422
|
+
const rawFm = split.data.rawFrontmatter;
|
|
2423
|
+
const stripped = body.replace(FENCE_RE2, "");
|
|
2424
|
+
const lines = stripped.split("\n");
|
|
2425
|
+
const inlineMarkers = [];
|
|
2426
|
+
let inSources = false;
|
|
2427
|
+
for (const line of lines) {
|
|
2428
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2429
|
+
inSources = true;
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
if (inSources) continue;
|
|
2433
|
+
for (const m of line.matchAll(INLINE_MARKER)) {
|
|
2434
|
+
inlineMarkers.push(m[0]);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (inlineMarkers.length === 0) {
|
|
2438
|
+
unresolved.push(relPath);
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const bodyLines = body.split("\n");
|
|
2442
|
+
let inSrc = false;
|
|
2443
|
+
const newBodyLines = [];
|
|
2444
|
+
for (const line of bodyLines) {
|
|
2445
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2446
|
+
inSrc = true;
|
|
2447
|
+
newBodyLines.push(line);
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if (inSrc) {
|
|
2451
|
+
newBodyLines.push(line);
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
INLINE_MARKER.lastIndex = 0;
|
|
2455
|
+
const lineWithoutMarkers = line.replace(INLINE_MARKER, "").trim();
|
|
2456
|
+
INLINE_MARKER.lastIndex = 0;
|
|
2457
|
+
if (lineWithoutMarkers.length === 0 && INLINE_MARKER.test(line)) {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
let cleaned = line;
|
|
2461
|
+
for (const marker of inlineMarkers) {
|
|
2462
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2463
|
+
const trailingRe = new RegExp(`([.!?]\\s*)${escapedMarker}`);
|
|
2464
|
+
if (trailingRe.test(cleaned)) {
|
|
2465
|
+
cleaned = cleaned.replace(trailingRe, "$1");
|
|
2466
|
+
}
|
|
2467
|
+
const midRe = new RegExp(`${escapedMarker}\\s*`);
|
|
2468
|
+
if (midRe.test(cleaned)) {
|
|
2469
|
+
cleaned = cleaned.replace(midRe, "");
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
newBodyLines.push(cleaned);
|
|
2473
|
+
}
|
|
2474
|
+
let newBody = newBodyLines.join("\n");
|
|
2475
|
+
const dedupedMarkers = [...new Set(inlineMarkers)];
|
|
2476
|
+
if (inSrc) {
|
|
2477
|
+
const existingSources = new Set(
|
|
2478
|
+
body.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
|
|
2479
|
+
);
|
|
2480
|
+
const newMarkers = dedupedMarkers.filter((m) => !existingSources.has(m));
|
|
2481
|
+
const sourceLines = newMarkers.map((m) => `- ${m}`);
|
|
2482
|
+
if (sourceLines.length > 0) {
|
|
2483
|
+
newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
|
|
2484
|
+
}
|
|
2485
|
+
} else {
|
|
2486
|
+
const sourceLines = dedupedMarkers.map((m) => `- ${m}`);
|
|
2487
|
+
newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
|
|
2488
|
+
}
|
|
2489
|
+
const newContent = `---
|
|
2490
|
+
${rawFm}
|
|
2491
|
+
---
|
|
2492
|
+
${newBody}`;
|
|
2493
|
+
await writeFile7(absPath, newContent, "utf8");
|
|
2494
|
+
fixed.push(relPath);
|
|
2495
|
+
} catch {
|
|
2496
|
+
unresolved.push(relPath);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (fixed.length > 0) {
|
|
2500
|
+
const fixedSet = new Set(fixed);
|
|
2501
|
+
const remaining = legacyPages.filter((p) => !fixedSet.has(p));
|
|
2502
|
+
if (remaining.length > 0) buckets.legacy_citation_style = remaining;
|
|
2503
|
+
else delete buckets.legacy_citation_style;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
if (input.fix && noOverview.length > 0) {
|
|
2507
|
+
for (const relPath of noOverview) {
|
|
2508
|
+
try {
|
|
2509
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
2510
|
+
const raw = await readFile13(absPath, "utf8");
|
|
2511
|
+
const split = splitFrontmatter(raw);
|
|
2512
|
+
if (!split.ok) {
|
|
2513
|
+
unresolved.push(relPath);
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
const body = split.data.body;
|
|
2517
|
+
const rawFm = split.data.rawFrontmatter;
|
|
2518
|
+
const fm = extractFrontmatter(raw);
|
|
2519
|
+
const title = fm.ok && typeof fm.data.title === "string" ? fm.data.title : "";
|
|
2520
|
+
const overviewSection = `## Overview
|
|
2521
|
+
|
|
2522
|
+
${title}`;
|
|
2523
|
+
const trimmedBody = body.replace(/^\n+/, "");
|
|
2524
|
+
const newContent = `---
|
|
2525
|
+
${rawFm}
|
|
2526
|
+
---
|
|
2527
|
+
|
|
2528
|
+
${overviewSection}
|
|
2529
|
+
|
|
2530
|
+
${trimmedBody}`;
|
|
2531
|
+
await writeFile7(absPath, newContent, "utf8");
|
|
2532
|
+
fixed.push(relPath);
|
|
2533
|
+
} catch {
|
|
2534
|
+
unresolved.push(relPath);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
const fixedBeforeOverview = fixed.length;
|
|
2538
|
+
const fixedSet = new Set(fixed);
|
|
2539
|
+
const remaining = noOverview.filter((p) => !fixedSet.has(p));
|
|
2540
|
+
if (remaining.length > 0) buckets.missing_overview = remaining;
|
|
2541
|
+
else delete buckets.missing_overview;
|
|
2542
|
+
}
|
|
2543
|
+
if (input.fix && wikilinkCitationFlags.length > 0) {
|
|
2544
|
+
const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
2545
|
+
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
2546
|
+
const wikilinkFixed = [];
|
|
2547
|
+
for (const relPath of wikilinkCitationFlags) {
|
|
2548
|
+
try {
|
|
2549
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
2550
|
+
const raw = await readFile13(absPath, "utf8");
|
|
2551
|
+
const split = splitFrontmatter(raw);
|
|
2552
|
+
if (!split.ok) {
|
|
2553
|
+
unresolved.push(relPath);
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
const body = split.data.body;
|
|
2557
|
+
const rawFm = split.data.rawFrontmatter;
|
|
2558
|
+
const stripped = body.replace(FENCE_RE2, "");
|
|
2559
|
+
const wikilinkMatches = [...stripped.matchAll(WIKILINK_RE)];
|
|
2560
|
+
if (wikilinkMatches.length === 0) {
|
|
2561
|
+
unresolved.push(relPath);
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2564
|
+
const wikilinkPaths = [...new Set(wikilinkMatches.map((m) => m[1]))];
|
|
2565
|
+
const bodyLines = body.split("\n");
|
|
2566
|
+
let inSrc = false;
|
|
2567
|
+
const newBodyLines = [];
|
|
2568
|
+
for (const line of bodyLines) {
|
|
2569
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2570
|
+
inSrc = true;
|
|
2571
|
+
newBodyLines.push(line);
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (inSrc) {
|
|
2575
|
+
newBodyLines.push(line);
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
let cleaned = line.replace(/\[\[raw\/[^\]|]+(?:\|[^\]]*)?\]\]/g, "");
|
|
2579
|
+
cleaned = cleaned.replace(/\s+\./g, ".").replace(/\s{2,}/g, " ").replace(/\s+$/, "");
|
|
2580
|
+
if (cleaned.length > 0 || line.trim().length === 0) {
|
|
2581
|
+
newBodyLines.push(cleaned);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
let newBody = newBodyLines.join("\n");
|
|
2585
|
+
const citationMarkers = wikilinkPaths.map((p) => `^[raw/${p}]`);
|
|
2586
|
+
const sourceEntries = extractSourceEntries(rawFm);
|
|
2587
|
+
const fmMarkers = [];
|
|
2588
|
+
for (const entry of sourceEntries) {
|
|
2589
|
+
let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
2590
|
+
rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
2591
|
+
if (rawPath.startsWith("raw/")) {
|
|
2592
|
+
fmMarkers.push(`^[${rawPath}]`);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
const allMarkers = [.../* @__PURE__ */ new Set([...citationMarkers, ...fmMarkers])];
|
|
2596
|
+
const hasSourcesSection = /^## Sources\b/m.test(newBody);
|
|
2597
|
+
if (hasSourcesSection) {
|
|
2598
|
+
const existingSources = new Set(
|
|
2599
|
+
newBody.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
|
|
2600
|
+
);
|
|
2601
|
+
const newMarkers = allMarkers.filter((m) => !existingSources.has(m));
|
|
2602
|
+
const sourceLines = newMarkers.map((m) => `- ${m}`);
|
|
2603
|
+
if (sourceLines.length > 0) {
|
|
2604
|
+
newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
|
|
2605
|
+
}
|
|
2606
|
+
} else {
|
|
2607
|
+
const sourceLines = allMarkers.map((m) => `- ${m}`);
|
|
2608
|
+
newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
|
|
2609
|
+
}
|
|
2610
|
+
const newContent = `---
|
|
2611
|
+
${rawFm}
|
|
2612
|
+
---
|
|
2613
|
+
${newBody}`;
|
|
2614
|
+
await writeFile7(absPath, newContent, "utf8");
|
|
2615
|
+
wikilinkFixed.push(relPath);
|
|
2616
|
+
} catch {
|
|
2617
|
+
unresolved.push(relPath);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
fixed.push(...wikilinkFixed);
|
|
2621
|
+
if (wikilinkFixed.length > 0) {
|
|
2622
|
+
const fixedSet = new Set(wikilinkFixed);
|
|
2623
|
+
const remaining = wikilinkCitationFlags.filter((p) => !fixedSet.has(p));
|
|
2624
|
+
if (remaining.length > 0) buckets.wikilink_citation = remaining;
|
|
2625
|
+
else delete buckets.wikilink_citation;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
1804
2628
|
}
|
|
1805
2629
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1806
2630
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1822,26 +2646,36 @@ async function runLint(input) {
|
|
|
1822
2646
|
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1823
2647
|
}
|
|
1824
2648
|
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
2649
|
+
if (input.fix && fixed.length > 0) {
|
|
2650
|
+
appendLastOp(input.vault, {
|
|
2651
|
+
operation: "lint-fix",
|
|
2652
|
+
summary: `fixed ${fixed.length} page(s)`,
|
|
2653
|
+
files: fixed,
|
|
2654
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
1825
2657
|
return {
|
|
1826
2658
|
exitCode,
|
|
1827
2659
|
result: ok({
|
|
1828
2660
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1829
2661
|
summary,
|
|
1830
2662
|
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
2663
|
+
fixed,
|
|
2664
|
+
unresolved,
|
|
1831
2665
|
humanHint: hintLines.join("\n")
|
|
1832
2666
|
})
|
|
1833
2667
|
};
|
|
1834
2668
|
}
|
|
1835
2669
|
|
|
1836
2670
|
// src/commands/config.ts
|
|
1837
|
-
import { readFile as
|
|
1838
|
-
import { existsSync } from "fs";
|
|
1839
|
-
import { join as
|
|
2671
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2672
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2673
|
+
import { join as join18 } from "path";
|
|
1840
2674
|
function validateKey(key) {
|
|
1841
2675
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
1842
2676
|
}
|
|
1843
2677
|
function configPath(home) {
|
|
1844
|
-
return
|
|
2678
|
+
return join18(home, ".skillwiki", ".env");
|
|
1845
2679
|
}
|
|
1846
2680
|
async function runConfigGet(input) {
|
|
1847
2681
|
if (!validateKey(input.key)) {
|
|
@@ -1859,7 +2693,7 @@ async function runConfigSet(input) {
|
|
|
1859
2693
|
try {
|
|
1860
2694
|
let originalContent;
|
|
1861
2695
|
try {
|
|
1862
|
-
originalContent = await
|
|
2696
|
+
originalContent = await readFile14(filePath, "utf8");
|
|
1863
2697
|
} catch {
|
|
1864
2698
|
}
|
|
1865
2699
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -1891,17 +2725,17 @@ async function runConfigList(input) {
|
|
|
1891
2725
|
}
|
|
1892
2726
|
async function runConfigPath(input) {
|
|
1893
2727
|
const filePath = configPath(input.home);
|
|
1894
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists:
|
|
2728
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync3(filePath), humanHint: filePath }) };
|
|
1895
2729
|
}
|
|
1896
2730
|
|
|
1897
2731
|
// src/commands/doctor.ts
|
|
1898
|
-
import { existsSync as
|
|
1899
|
-
import { join as
|
|
2732
|
+
import { existsSync as existsSync5, lstatSync, readlinkSync, readdirSync, statSync } from "fs";
|
|
2733
|
+
import { join as join21, resolve as resolve4 } from "path";
|
|
1900
2734
|
import { execSync } from "child_process";
|
|
1901
2735
|
|
|
1902
2736
|
// src/utils/auto-update.ts
|
|
1903
|
-
import { readFileSync as
|
|
1904
|
-
import { join as
|
|
2737
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
2738
|
+
import { join as join19, dirname as dirname8 } from "path";
|
|
1905
2739
|
import { spawn } from "child_process";
|
|
1906
2740
|
|
|
1907
2741
|
// src/utils/update-consts.ts
|
|
@@ -1912,11 +2746,11 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
|
1912
2746
|
|
|
1913
2747
|
// src/utils/auto-update.ts
|
|
1914
2748
|
function cachePath(home) {
|
|
1915
|
-
return
|
|
2749
|
+
return join19(home, ".skillwiki", CACHE_FILENAME);
|
|
1916
2750
|
}
|
|
1917
2751
|
function readCacheRaw(home) {
|
|
1918
2752
|
try {
|
|
1919
|
-
const raw =
|
|
2753
|
+
const raw = readFileSync4(cachePath(home), "utf8");
|
|
1920
2754
|
return JSON.parse(raw);
|
|
1921
2755
|
} catch {
|
|
1922
2756
|
return null;
|
|
@@ -1931,8 +2765,8 @@ function readCache(home) {
|
|
|
1931
2765
|
}
|
|
1932
2766
|
function writeCache(home, cache) {
|
|
1933
2767
|
const p = cachePath(home);
|
|
1934
|
-
|
|
1935
|
-
|
|
2768
|
+
mkdirSync2(dirname8(p), { recursive: true });
|
|
2769
|
+
writeFileSync3(p, JSON.stringify(cache, null, 2));
|
|
1936
2770
|
}
|
|
1937
2771
|
function latestFromCache(home, currentVersion) {
|
|
1938
2772
|
const { cache } = readCache(home);
|
|
@@ -1950,7 +2784,7 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
1950
2784
|
const { isStale } = readCache(home);
|
|
1951
2785
|
if (!isStale) return;
|
|
1952
2786
|
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
1953
|
-
if (!
|
|
2787
|
+
if (!existsSync4(bgScript)) return;
|
|
1954
2788
|
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
1955
2789
|
detached: true,
|
|
1956
2790
|
stdio: "ignore"
|
|
@@ -1961,13 +2795,13 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
1961
2795
|
}
|
|
1962
2796
|
|
|
1963
2797
|
// src/utils/plugin-registry.ts
|
|
1964
|
-
import { readFileSync as
|
|
1965
|
-
import { join as
|
|
1966
|
-
var REGISTRY_PATH =
|
|
2798
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2799
|
+
import { join as join20 } from "path";
|
|
2800
|
+
var REGISTRY_PATH = join20(".claude", "plugins", "installed_plugins.json");
|
|
1967
2801
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
1968
2802
|
function readInstalledPlugins(home) {
|
|
1969
2803
|
try {
|
|
1970
|
-
const raw =
|
|
2804
|
+
const raw = readFileSync5(join20(home, REGISTRY_PATH), "utf8");
|
|
1971
2805
|
return JSON.parse(raw);
|
|
1972
2806
|
} catch {
|
|
1973
2807
|
return null;
|
|
@@ -1992,23 +2826,83 @@ function checkNodeVersion() {
|
|
|
1992
2826
|
}
|
|
1993
2827
|
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1994
2828
|
}
|
|
1995
|
-
function
|
|
2829
|
+
function detectCliChannels(argv, home) {
|
|
2830
|
+
const channels = [];
|
|
1996
2831
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1997
|
-
|
|
2832
|
+
const devPath = resolve4(argv[1]);
|
|
2833
|
+
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
2834
|
+
}
|
|
2835
|
+
try {
|
|
2836
|
+
const whichOut = execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
2837
|
+
if (whichOut) {
|
|
2838
|
+
const isDev = isDevSymlink(whichOut);
|
|
2839
|
+
if (!channels.some((c) => c.path === resolve4(whichOut))) {
|
|
2840
|
+
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
} catch {
|
|
2844
|
+
}
|
|
2845
|
+
const plugin = findPlugin(home);
|
|
2846
|
+
if (plugin) {
|
|
2847
|
+
const pluginBin = join21(plugin.installPath, "bin", "skillwiki");
|
|
2848
|
+
if (existsSync5(pluginBin)) {
|
|
2849
|
+
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
2850
|
+
}
|
|
1998
2851
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2852
|
+
const installBin = join21(home, ".claude", "skills", "bin", "skillwiki");
|
|
2853
|
+
if (existsSync5(installBin)) {
|
|
2854
|
+
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
2001
2855
|
}
|
|
2856
|
+
return channels;
|
|
2857
|
+
}
|
|
2858
|
+
function isDevSymlink(binPath) {
|
|
2002
2859
|
try {
|
|
2003
|
-
|
|
2004
|
-
|
|
2860
|
+
const st = lstatSync(binPath);
|
|
2861
|
+
if (st.isSymbolicLink()) {
|
|
2862
|
+
const target = resolve4(binPath, "..", readlinkSync(binPath));
|
|
2863
|
+
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
2864
|
+
}
|
|
2005
2865
|
} catch {
|
|
2006
|
-
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
2007
2866
|
}
|
|
2867
|
+
return false;
|
|
2868
|
+
}
|
|
2869
|
+
function checkCliChannels(argv, home) {
|
|
2870
|
+
const channels = detectCliChannels(argv, home);
|
|
2871
|
+
if (channels.length === 0) {
|
|
2872
|
+
return check("warn", "cli_channels", "CLI channels", "skillwiki not found on any channel");
|
|
2873
|
+
}
|
|
2874
|
+
if (channels.length === 1) {
|
|
2875
|
+
const ch = channels[0];
|
|
2876
|
+
const label = ch.isDevLink ? `${ch.name} (dev source)` : ch.name;
|
|
2877
|
+
return check("pass", "cli_channels", "CLI channels", `Single channel: ${label}`);
|
|
2878
|
+
}
|
|
2879
|
+
const devChannels = channels.filter((c) => c.isDevLink);
|
|
2880
|
+
const prodChannels = channels.filter((c) => !c.isDevLink);
|
|
2881
|
+
if (devChannels.length > 0 && prodChannels.length > 0) {
|
|
2882
|
+
const devNames = devChannels.map((c) => `${c.name}(dev)`);
|
|
2883
|
+
const prodNames = prodChannels.map((c) => c.name);
|
|
2884
|
+
return check(
|
|
2885
|
+
"warn",
|
|
2886
|
+
"cli_channels",
|
|
2887
|
+
"CLI channels",
|
|
2888
|
+
`${channels.length} channels: ${[...devNames, ...prodNames].join(", ")} \u2014 dev and prod binaries overlap; dev repo should use project-local settings only`
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
const names = channels.map((c) => c.name);
|
|
2892
|
+
const hasInstall = channels.some((c) => c.name === "install");
|
|
2893
|
+
if (hasInstall) {
|
|
2894
|
+
return check(
|
|
2895
|
+
"warn",
|
|
2896
|
+
"cli_channels",
|
|
2897
|
+
"CLI channels",
|
|
2898
|
+
`${channels.length} channels: ${names.join(", ")} \u2014 remove unused install with: rm ~/.claude/skills/bin/skillwiki`
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${names.join(", ")}`);
|
|
2008
2902
|
}
|
|
2009
2903
|
async function checkConfigFile(home) {
|
|
2010
2904
|
const cfgPath = configPath(home);
|
|
2011
|
-
if (!
|
|
2905
|
+
if (!existsSync5(cfgPath)) {
|
|
2012
2906
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
2013
2907
|
}
|
|
2014
2908
|
try {
|
|
@@ -2023,7 +2917,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
2023
2917
|
if (resolvedPath === void 0) {
|
|
2024
2918
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
2025
2919
|
}
|
|
2026
|
-
if (
|
|
2920
|
+
if (existsSync5(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
2027
2921
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
2028
2922
|
}
|
|
2029
2923
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -2032,20 +2926,27 @@ function checkVaultStructure(resolvedPath) {
|
|
|
2032
2926
|
if (resolvedPath === void 0) {
|
|
2033
2927
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
2034
2928
|
}
|
|
2035
|
-
if (!
|
|
2929
|
+
if (!existsSync5(resolvedPath)) {
|
|
2036
2930
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
2037
2931
|
}
|
|
2038
2932
|
const missing = [];
|
|
2039
|
-
if (!
|
|
2933
|
+
if (!existsSync5(join21(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
2040
2934
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
2041
|
-
if (!
|
|
2935
|
+
if (!existsSync5(join21(resolvedPath, dir))) missing.push(dir + "/");
|
|
2042
2936
|
}
|
|
2043
2937
|
if (missing.length === 0) {
|
|
2044
2938
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
2045
2939
|
}
|
|
2046
2940
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
2047
2941
|
}
|
|
2048
|
-
function checkSkillsInstalled(home) {
|
|
2942
|
+
function checkSkillsInstalled(home, cwd) {
|
|
2943
|
+
const srcDir = cwd ? join21(cwd, "packages", "skills") : void 0;
|
|
2944
|
+
if (srcDir && existsSync5(srcDir)) {
|
|
2945
|
+
const found = findSkillMd(srcDir);
|
|
2946
|
+
if (found.length > 0) {
|
|
2947
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2049
2950
|
const plugin = findPlugin(home);
|
|
2050
2951
|
if (plugin) {
|
|
2051
2952
|
const found = findSkillMd(plugin.installPath);
|
|
@@ -2053,8 +2954,8 @@ function checkSkillsInstalled(home) {
|
|
|
2053
2954
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
2054
2955
|
}
|
|
2055
2956
|
}
|
|
2056
|
-
const skillsDir =
|
|
2057
|
-
if (
|
|
2957
|
+
const skillsDir = join21(home, ".claude", "skills");
|
|
2958
|
+
if (existsSync5(skillsDir)) {
|
|
2058
2959
|
const found = findSkillMd(skillsDir);
|
|
2059
2960
|
if (found.length > 0) {
|
|
2060
2961
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
@@ -2062,6 +2963,25 @@ function checkSkillsInstalled(home) {
|
|
|
2062
2963
|
}
|
|
2063
2964
|
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
|
|
2064
2965
|
}
|
|
2966
|
+
function checkDuplicateSkills(home) {
|
|
2967
|
+
const plugin = findPlugin(home);
|
|
2968
|
+
const skillsDir = join21(home, ".claude", "skills");
|
|
2969
|
+
if (!plugin || !existsSync5(skillsDir)) {
|
|
2970
|
+
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
2971
|
+
}
|
|
2972
|
+
const pluginSkills = findSkillNames(plugin.installPath);
|
|
2973
|
+
const cliSkills = findSkillNames(skillsDir);
|
|
2974
|
+
const duplicates = pluginSkills.filter((name) => cliSkills.includes(name));
|
|
2975
|
+
if (duplicates.length === 0) {
|
|
2976
|
+
return check("pass", "skills_duplicate", "Skills not duplicated", "No overlap between plugin and CLI install");
|
|
2977
|
+
}
|
|
2978
|
+
return check(
|
|
2979
|
+
"warn",
|
|
2980
|
+
"skills_duplicate",
|
|
2981
|
+
"Skills not duplicated",
|
|
2982
|
+
`${duplicates.length} skill(s) in both plugin and ~/.claude/skills/ \u2014 remove CLI copies: rm -r ~/.claude/skills/{${duplicates.slice(0, 3).join(",")}${duplicates.length > 3 ? ",\u2026" : ""}}`
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2065
2985
|
function checkNpmUpdate(home, currentVersion) {
|
|
2066
2986
|
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
2067
2987
|
if (!latest) {
|
|
@@ -2111,12 +3031,112 @@ async function checkProfiles(home) {
|
|
|
2111
3031
|
}
|
|
2112
3032
|
async function checkProjectLocalOverride(cwd) {
|
|
2113
3033
|
const dir = cwd ?? process.cwd();
|
|
2114
|
-
const envPath =
|
|
2115
|
-
if (
|
|
3034
|
+
const envPath = join21(dir, ".skillwiki", ".env");
|
|
3035
|
+
if (existsSync5(envPath)) {
|
|
2116
3036
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
2117
3037
|
}
|
|
2118
3038
|
return check("pass", "project_local", "Project-local config", "None");
|
|
2119
3039
|
}
|
|
3040
|
+
function checkVaultGitRemote(resolvedPath) {
|
|
3041
|
+
if (resolvedPath === void 0) {
|
|
3042
|
+
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3043
|
+
}
|
|
3044
|
+
if (!existsSync5(join21(resolvedPath, ".git"))) {
|
|
3045
|
+
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
3046
|
+
}
|
|
3047
|
+
try {
|
|
3048
|
+
const remote = execSync("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3049
|
+
if (!remote) {
|
|
3050
|
+
return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
|
|
3051
|
+
}
|
|
3052
|
+
let branch = "(no commits yet)";
|
|
3053
|
+
try {
|
|
3054
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3055
|
+
} catch {
|
|
3056
|
+
}
|
|
3057
|
+
return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
|
|
3058
|
+
} catch {
|
|
3059
|
+
return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
function checkObsidianTemplates(resolvedPath) {
|
|
3063
|
+
if (resolvedPath === void 0) {
|
|
3064
|
+
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3065
|
+
}
|
|
3066
|
+
const missing = [];
|
|
3067
|
+
if (!existsSync5(join21(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
3068
|
+
if (!existsSync5(join21(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
3069
|
+
if (!existsSync5(join21(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
3070
|
+
if (missing.length === 0) {
|
|
3071
|
+
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
3072
|
+
}
|
|
3073
|
+
return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
|
|
3074
|
+
}
|
|
3075
|
+
function checkDotStoreClean(resolvedPath) {
|
|
3076
|
+
if (resolvedPath === void 0) {
|
|
3077
|
+
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3078
|
+
}
|
|
3079
|
+
const rawDir = join21(resolvedPath, "raw");
|
|
3080
|
+
if (!existsSync5(rawDir)) {
|
|
3081
|
+
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
3082
|
+
}
|
|
3083
|
+
const found = [];
|
|
3084
|
+
(function walk2(dir, rel) {
|
|
3085
|
+
let entries;
|
|
3086
|
+
try {
|
|
3087
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3088
|
+
} catch {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
for (const entry of entries) {
|
|
3092
|
+
if (entry.name === ".DS_Store") {
|
|
3093
|
+
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
3094
|
+
} else if (entry.isDirectory()) {
|
|
3095
|
+
walk2(join21(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
})(rawDir, "");
|
|
3099
|
+
if (found.length === 0) {
|
|
3100
|
+
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
|
|
3101
|
+
}
|
|
3102
|
+
return check("warn", "dsstore_clean", "No .DS_Store in raw/", `${found.length} .DS_Store file(s) found \u2014 remove with: find ${rawDir} -name .DS_Store -delete`);
|
|
3103
|
+
}
|
|
3104
|
+
function checkSyncLastPush(resolvedPath) {
|
|
3105
|
+
if (resolvedPath === void 0) {
|
|
3106
|
+
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3107
|
+
}
|
|
3108
|
+
if (!existsSync5(join21(resolvedPath, ".git"))) {
|
|
3109
|
+
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
3110
|
+
}
|
|
3111
|
+
let timestamp;
|
|
3112
|
+
try {
|
|
3113
|
+
const out = execSync("git log -1 --format=%ct origin/HEAD", {
|
|
3114
|
+
cwd: resolvedPath,
|
|
3115
|
+
encoding: "utf8",
|
|
3116
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3117
|
+
}).trim();
|
|
3118
|
+
timestamp = parseInt(out, 10);
|
|
3119
|
+
} catch {
|
|
3120
|
+
try {
|
|
3121
|
+
const out = execSync("git log -1 --format=%ct HEAD", {
|
|
3122
|
+
cwd: resolvedPath,
|
|
3123
|
+
encoding: "utf8",
|
|
3124
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3125
|
+
}).trim();
|
|
3126
|
+
timestamp = parseInt(out, 10);
|
|
3127
|
+
} catch {
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
if (timestamp === void 0 || isNaN(timestamp)) {
|
|
3131
|
+
return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
|
|
3132
|
+
}
|
|
3133
|
+
const daysSince2 = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
|
|
3134
|
+
const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
|
|
3135
|
+
if (daysSince2 > 7) {
|
|
3136
|
+
return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince2} days ago \u2014 consider running \`skillwiki sync status\``);
|
|
3137
|
+
}
|
|
3138
|
+
return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince2} day(s) ago)`);
|
|
3139
|
+
}
|
|
2120
3140
|
function findSkillMd(dir) {
|
|
2121
3141
|
const results = [];
|
|
2122
3142
|
let entries;
|
|
@@ -2127,9 +3147,24 @@ function findSkillMd(dir) {
|
|
|
2127
3147
|
}
|
|
2128
3148
|
for (const entry of entries) {
|
|
2129
3149
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
2130
|
-
results.push(
|
|
3150
|
+
results.push(join21(dir, entry.name));
|
|
2131
3151
|
} else if (entry.isDirectory()) {
|
|
2132
|
-
results.push(...findSkillMd(
|
|
3152
|
+
results.push(...findSkillMd(join21(dir, entry.name)));
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
return results;
|
|
3156
|
+
}
|
|
3157
|
+
function findSkillNames(dir) {
|
|
3158
|
+
const results = [];
|
|
3159
|
+
let entries;
|
|
3160
|
+
try {
|
|
3161
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3162
|
+
} catch {
|
|
3163
|
+
return results;
|
|
3164
|
+
}
|
|
3165
|
+
for (const entry of entries) {
|
|
3166
|
+
if (entry.isDirectory() && existsSync5(join21(dir, entry.name, "SKILL.md"))) {
|
|
3167
|
+
results.push(entry.name);
|
|
2133
3168
|
}
|
|
2134
3169
|
}
|
|
2135
3170
|
return results;
|
|
@@ -2137,7 +3172,7 @@ function findSkillMd(dir) {
|
|
|
2137
3172
|
async function runDoctor(input) {
|
|
2138
3173
|
const checks = [];
|
|
2139
3174
|
checks.push(checkNodeVersion());
|
|
2140
|
-
checks.push(
|
|
3175
|
+
checks.push(checkCliChannels(input.argv, input.home));
|
|
2141
3176
|
checks.push(await checkConfigFile(input.home));
|
|
2142
3177
|
checks.push(await checkProfiles(input.home));
|
|
2143
3178
|
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
@@ -2150,7 +3185,12 @@ async function runDoctor(input) {
|
|
|
2150
3185
|
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
2151
3186
|
checks.push(checkWikiPathExists(resolvedPath));
|
|
2152
3187
|
checks.push(checkVaultStructure(resolvedPath));
|
|
2153
|
-
checks.push(
|
|
3188
|
+
checks.push(checkObsidianTemplates(resolvedPath));
|
|
3189
|
+
checks.push(checkVaultGitRemote(resolvedPath));
|
|
3190
|
+
checks.push(checkSyncLastPush(resolvedPath));
|
|
3191
|
+
checks.push(checkDotStoreClean(resolvedPath));
|
|
3192
|
+
checks.push(checkSkillsInstalled(input.home, input.cwd));
|
|
3193
|
+
checks.push(checkDuplicateSkills(input.home));
|
|
2154
3194
|
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2155
3195
|
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
2156
3196
|
const summary = {
|
|
@@ -2172,8 +3212,8 @@ async function runDoctor(input) {
|
|
|
2172
3212
|
}
|
|
2173
3213
|
|
|
2174
3214
|
// src/commands/archive.ts
|
|
2175
|
-
import { rename as
|
|
2176
|
-
import { join as
|
|
3215
|
+
import { rename as rename4, mkdir as mkdir7, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
3216
|
+
import { join as join22, dirname as dirname9 } from "path";
|
|
2177
3217
|
async function runArchive(input) {
|
|
2178
3218
|
const scan = await scanVault(input.vault);
|
|
2179
3219
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -2189,31 +3229,37 @@ async function runArchive(input) {
|
|
|
2189
3229
|
}
|
|
2190
3230
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2191
3231
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2192
|
-
const archivePath =
|
|
2193
|
-
await
|
|
3232
|
+
const archivePath = join22("_archive", relPath).replace(/\\/g, "/");
|
|
3233
|
+
await mkdir7(dirname9(join22(input.vault, archivePath)), { recursive: true });
|
|
2194
3234
|
let indexUpdated = false;
|
|
2195
3235
|
if (!isRaw) {
|
|
2196
|
-
const indexPath =
|
|
3236
|
+
const indexPath = join22(input.vault, "index.md");
|
|
2197
3237
|
try {
|
|
2198
|
-
const idx = await
|
|
3238
|
+
const idx = await readFile15(indexPath, "utf8");
|
|
2199
3239
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2200
3240
|
const originalLines = idx.split("\n");
|
|
2201
3241
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2202
3242
|
if (filtered.length !== originalLines.length) {
|
|
2203
|
-
await
|
|
3243
|
+
await writeFile8(indexPath, filtered.join("\n"), "utf8");
|
|
2204
3244
|
indexUpdated = true;
|
|
2205
3245
|
}
|
|
2206
3246
|
} catch (e) {
|
|
2207
|
-
if (e
|
|
3247
|
+
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
2208
3248
|
}
|
|
2209
3249
|
}
|
|
2210
|
-
await
|
|
3250
|
+
await rename4(join22(input.vault, relPath), join22(input.vault, archivePath));
|
|
3251
|
+
appendLastOp(input.vault, {
|
|
3252
|
+
operation: "archive",
|
|
3253
|
+
summary: `moved ${relPath} to ${archivePath}`,
|
|
3254
|
+
files: [relPath],
|
|
3255
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3256
|
+
});
|
|
2211
3257
|
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
2212
3258
|
}
|
|
2213
3259
|
|
|
2214
3260
|
// src/commands/drift.ts
|
|
2215
3261
|
import { createHash as createHash2 } from "crypto";
|
|
2216
|
-
import { writeFile as
|
|
3262
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2217
3263
|
|
|
2218
3264
|
// src/utils/fetch.ts
|
|
2219
3265
|
async function controlledFetch(url, opts) {
|
|
@@ -2228,7 +3274,7 @@ async function controlledFetch(url, opts) {
|
|
|
2228
3274
|
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
2229
3275
|
} catch (e) {
|
|
2230
3276
|
clearTimeout(timer);
|
|
2231
|
-
if (e
|
|
3277
|
+
if (e instanceof Error && e.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
2232
3278
|
return err("FETCH_FAILED", { message: String(e) });
|
|
2233
3279
|
}
|
|
2234
3280
|
clearTimeout(timer);
|
|
@@ -2279,6 +3325,7 @@ async function runDrift(input) {
|
|
|
2279
3325
|
const sourceUrl = sourceUrlMatch[1].trim();
|
|
2280
3326
|
const storedHash = storedHashMatch[1];
|
|
2281
3327
|
if (!sourceUrl.startsWith("http://") && !sourceUrl.startsWith("https://")) continue;
|
|
3328
|
+
if (/^refreshable:\s*false\b/m.test(rawFrontmatter)) continue;
|
|
2282
3329
|
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2283
3330
|
if (!resp.ok) {
|
|
2284
3331
|
results.push({
|
|
@@ -2299,7 +3346,7 @@ async function runDrift(input) {
|
|
|
2299
3346
|
${newFm}
|
|
2300
3347
|
---
|
|
2301
3348
|
${body}`;
|
|
2302
|
-
await
|
|
3349
|
+
await writeFile9(raw.absPath, newText, "utf8");
|
|
2303
3350
|
results.push({
|
|
2304
3351
|
raw_path: raw.relPath,
|
|
2305
3352
|
source_url: sourceUrl,
|
|
@@ -2327,6 +3374,14 @@ ${body}`;
|
|
|
2327
3374
|
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2328
3375
|
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2329
3376
|
if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
|
|
3377
|
+
if (input.apply && updated.length > 0) {
|
|
3378
|
+
appendLastOp(input.vault, {
|
|
3379
|
+
operation: "drift-apply",
|
|
3380
|
+
summary: `updated ${updated.length} raw sources`,
|
|
3381
|
+
files: updated.map((u) => u.raw_path),
|
|
3382
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
2330
3385
|
return {
|
|
2331
3386
|
exitCode,
|
|
2332
3387
|
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
|
|
@@ -2334,7 +3389,7 @@ ${body}`;
|
|
|
2334
3389
|
}
|
|
2335
3390
|
|
|
2336
3391
|
// src/commands/migrate-citations.ts
|
|
2337
|
-
import { writeFile as
|
|
3392
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
2338
3393
|
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2339
3394
|
function moveMarkersToParagraphEnd(body) {
|
|
2340
3395
|
const lines = body.split("\n");
|
|
@@ -2457,7 +3512,7 @@ ${migratedBody}${newFooter}`;
|
|
|
2457
3512
|
continue;
|
|
2458
3513
|
}
|
|
2459
3514
|
if (!input.dryRun) {
|
|
2460
|
-
await
|
|
3515
|
+
await writeFile10(page.absPath, newText, "utf8");
|
|
2461
3516
|
}
|
|
2462
3517
|
migrated.push(page.relPath);
|
|
2463
3518
|
}
|
|
@@ -2466,6 +3521,14 @@ ${migratedBody}${newFooter}`;
|
|
|
2466
3521
|
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2467
3522
|
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2468
3523
|
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
3524
|
+
if (!input.dryRun && migrated.length > 0) {
|
|
3525
|
+
appendLastOp(input.vault, {
|
|
3526
|
+
operation: "migrate-citations",
|
|
3527
|
+
summary: `converted ${migrated.length} citation(s)`,
|
|
3528
|
+
files: migrated,
|
|
3529
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
2469
3532
|
return {
|
|
2470
3533
|
exitCode,
|
|
2471
3534
|
result: ok({
|
|
@@ -2479,7 +3542,7 @@ ${migratedBody}${newFooter}`;
|
|
|
2479
3542
|
}
|
|
2480
3543
|
|
|
2481
3544
|
// src/commands/frontmatter-fix.ts
|
|
2482
|
-
import { writeFile as
|
|
3545
|
+
import { writeFile as writeFile11 } from "fs/promises";
|
|
2483
3546
|
function isoToday() {
|
|
2484
3547
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2485
3548
|
}
|
|
@@ -2521,7 +3584,7 @@ ${newBody}`;
|
|
|
2521
3584
|
continue;
|
|
2522
3585
|
}
|
|
2523
3586
|
if (!input.dryRun) {
|
|
2524
|
-
await
|
|
3587
|
+
await writeFile11(page.absPath, newText, "utf8");
|
|
2525
3588
|
}
|
|
2526
3589
|
fixed.push(page.relPath);
|
|
2527
3590
|
}
|
|
@@ -2531,6 +3594,14 @@ ${newBody}`;
|
|
|
2531
3594
|
if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
|
|
2532
3595
|
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
2533
3596
|
if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
3597
|
+
if (!input.dryRun && fixed.length > 0) {
|
|
3598
|
+
appendLastOp(input.vault, {
|
|
3599
|
+
operation: "frontmatter-fix",
|
|
3600
|
+
summary: `normalized frontmatter on ${fixed.length} page(s)`,
|
|
3601
|
+
files: fixed,
|
|
3602
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
2534
3605
|
return {
|
|
2535
3606
|
exitCode,
|
|
2536
3607
|
result: ok({
|
|
@@ -2545,13 +3616,41 @@ ${newBody}`;
|
|
|
2545
3616
|
|
|
2546
3617
|
// src/commands/update.ts
|
|
2547
3618
|
import { execSync as execSync2 } from "child_process";
|
|
2548
|
-
import { readFileSync as
|
|
3619
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3620
|
+
import { join as join23 } from "path";
|
|
3621
|
+
function resolveGlobalSkillsRoot() {
|
|
3622
|
+
try {
|
|
3623
|
+
const globalRoot = execSync2("npm root -g", {
|
|
3624
|
+
encoding: "utf8",
|
|
3625
|
+
timeout: 5e3
|
|
3626
|
+
}).trim();
|
|
3627
|
+
return join23(globalRoot, "skillwiki", "skills");
|
|
3628
|
+
} catch {
|
|
3629
|
+
return null;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
async function refreshInstalledSkills(target) {
|
|
3633
|
+
const skillsRoot = resolveGlobalSkillsRoot();
|
|
3634
|
+
if (!skillsRoot) {
|
|
3635
|
+
return { warnings: ["could not locate global skillwiki installation for skill refresh"], refreshed: false };
|
|
3636
|
+
}
|
|
3637
|
+
try {
|
|
3638
|
+
const result = await runInstall({ skillsRoot, target, dryRun: false, symlink: false });
|
|
3639
|
+
if (result.result.ok) {
|
|
3640
|
+
return { warnings: result.result.data.version_warnings, refreshed: true };
|
|
3641
|
+
}
|
|
3642
|
+
return { warnings: [`skill refresh failed: ${result.result.error}`], refreshed: false };
|
|
3643
|
+
} catch (e) {
|
|
3644
|
+
return { warnings: [`skill refresh error: ${String(e)}`], refreshed: false };
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
2549
3647
|
async function runUpdate(input) {
|
|
2550
3648
|
const pkg2 = JSON.parse(
|
|
2551
|
-
|
|
3649
|
+
readFileSync6(new URL("../../package.json", import.meta.url), "utf8")
|
|
2552
3650
|
);
|
|
2553
3651
|
const currentVersion = pkg2.version;
|
|
2554
3652
|
const tag = input.distTag ?? "beta";
|
|
3653
|
+
const target = join23(input.home, ".claude", "skills");
|
|
2555
3654
|
let latest;
|
|
2556
3655
|
try {
|
|
2557
3656
|
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
@@ -2577,6 +3676,8 @@ async function runUpdate(input) {
|
|
|
2577
3676
|
previousVersion: currentVersion,
|
|
2578
3677
|
newVersion: null,
|
|
2579
3678
|
wasAlreadyLatest: true,
|
|
3679
|
+
version_warnings: [],
|
|
3680
|
+
skills_refreshed: false,
|
|
2580
3681
|
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2581
3682
|
})
|
|
2582
3683
|
};
|
|
@@ -2593,33 +3694,187 @@ async function runUpdate(input) {
|
|
|
2593
3694
|
};
|
|
2594
3695
|
}
|
|
2595
3696
|
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
3697
|
+
const installResult = await refreshInstalledSkills(target);
|
|
3698
|
+
const version_warnings = installResult.warnings;
|
|
3699
|
+
const skills_refreshed = installResult.refreshed;
|
|
3700
|
+
const hintLines = [
|
|
3701
|
+
`Updated skillwiki ${currentVersion} \u2192 ${latest}`,
|
|
3702
|
+
`skills refreshed: ${skills_refreshed}`
|
|
3703
|
+
];
|
|
3704
|
+
if (version_warnings.length > 0) {
|
|
3705
|
+
hintLines.push(`version warnings: ${version_warnings.length}`);
|
|
3706
|
+
for (const w of version_warnings) hintLines.push(` ${w}`);
|
|
3707
|
+
}
|
|
2596
3708
|
return {
|
|
2597
3709
|
exitCode: ExitCode.OK,
|
|
2598
3710
|
result: ok({
|
|
2599
3711
|
previousVersion: currentVersion,
|
|
2600
3712
|
newVersion: latest,
|
|
2601
3713
|
wasAlreadyLatest: false,
|
|
2602
|
-
|
|
3714
|
+
version_warnings,
|
|
3715
|
+
skills_refreshed,
|
|
3716
|
+
humanHint: hintLines.join("\n")
|
|
3717
|
+
})
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
// src/commands/self-update.ts
|
|
3722
|
+
import { execSync as execSync3 } from "child_process";
|
|
3723
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
3724
|
+
import { join as join24 } from "path";
|
|
3725
|
+
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
3726
|
+
async function runSelfUpdate(input) {
|
|
3727
|
+
const currentVersion = JSON.parse(
|
|
3728
|
+
readFileSync7(new URL("../../package.json", import.meta.url), "utf8")
|
|
3729
|
+
).version;
|
|
3730
|
+
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
3731
|
+
const localPkgPath = join24(sourceRoot, "packages", "cli", "package.json");
|
|
3732
|
+
const hasLocalSource = existsSync6(localPkgPath);
|
|
3733
|
+
if (input.check) {
|
|
3734
|
+
let availableVersion = null;
|
|
3735
|
+
let source;
|
|
3736
|
+
if (hasLocalSource) {
|
|
3737
|
+
source = "local";
|
|
3738
|
+
try {
|
|
3739
|
+
availableVersion = JSON.parse(readFileSync7(localPkgPath, "utf8")).version ?? null;
|
|
3740
|
+
} catch {
|
|
3741
|
+
availableVersion = null;
|
|
3742
|
+
}
|
|
3743
|
+
} else {
|
|
3744
|
+
source = "npm";
|
|
3745
|
+
try {
|
|
3746
|
+
availableVersion = execSync3("npm view skillwiki@beta version", {
|
|
3747
|
+
encoding: "utf8",
|
|
3748
|
+
timeout: 15e3
|
|
3749
|
+
}).trim();
|
|
3750
|
+
} catch (e) {
|
|
3751
|
+
return {
|
|
3752
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3753
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
const updateAvailable = availableVersion !== null && availableVersion !== currentVersion;
|
|
3758
|
+
const hint = updateAvailable ? `Update available: ${currentVersion} \u2192 ${availableVersion} (${source})` : `Already up to date: v${currentVersion} (${source})`;
|
|
3759
|
+
return {
|
|
3760
|
+
exitCode: ExitCode.OK,
|
|
3761
|
+
result: ok({
|
|
3762
|
+
source,
|
|
3763
|
+
currentVersion,
|
|
3764
|
+
availableVersion,
|
|
3765
|
+
updateAvailable,
|
|
3766
|
+
humanHint: hint
|
|
3767
|
+
})
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
if (hasLocalSource) {
|
|
3771
|
+
try {
|
|
3772
|
+
execSync3("npm run build -w packages/cli", {
|
|
3773
|
+
cwd: sourceRoot,
|
|
3774
|
+
stdio: "pipe",
|
|
3775
|
+
timeout: 6e4
|
|
3776
|
+
});
|
|
3777
|
+
} catch (e) {
|
|
3778
|
+
return {
|
|
3779
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3780
|
+
result: err("BUILD_FAILED", { message: `Build failed: ${String(e)}` })
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
try {
|
|
3784
|
+
execSync3("npm link ./packages/cli", {
|
|
3785
|
+
cwd: sourceRoot,
|
|
3786
|
+
stdio: "pipe",
|
|
3787
|
+
timeout: 3e4
|
|
3788
|
+
});
|
|
3789
|
+
} catch (e) {
|
|
3790
|
+
return {
|
|
3791
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3792
|
+
result: err("LINK_FAILED", { message: `npm link failed: ${String(e)}` })
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
const newVersion = (() => {
|
|
3796
|
+
try {
|
|
3797
|
+
return JSON.parse(readFileSync7(localPkgPath, "utf8")).version ?? "unknown";
|
|
3798
|
+
} catch {
|
|
3799
|
+
return "unknown";
|
|
3800
|
+
}
|
|
3801
|
+
})();
|
|
3802
|
+
return {
|
|
3803
|
+
exitCode: ExitCode.OK,
|
|
3804
|
+
result: ok({
|
|
3805
|
+
source: "local",
|
|
3806
|
+
currentVersion,
|
|
3807
|
+
availableVersion: newVersion,
|
|
3808
|
+
updateAvailable: newVersion !== currentVersion,
|
|
3809
|
+
newVersion,
|
|
3810
|
+
humanHint: `Built and linked from local source: v${newVersion}`
|
|
3811
|
+
})
|
|
3812
|
+
};
|
|
3813
|
+
}
|
|
3814
|
+
let latestVersion;
|
|
3815
|
+
try {
|
|
3816
|
+
latestVersion = execSync3("npm view skillwiki@beta version", {
|
|
3817
|
+
encoding: "utf8",
|
|
3818
|
+
timeout: 15e3
|
|
3819
|
+
}).trim();
|
|
3820
|
+
} catch (e) {
|
|
3821
|
+
return {
|
|
3822
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3823
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
if (latestVersion === currentVersion) {
|
|
3827
|
+
return {
|
|
3828
|
+
exitCode: ExitCode.OK,
|
|
3829
|
+
result: ok({
|
|
3830
|
+
source: "npm",
|
|
3831
|
+
currentVersion,
|
|
3832
|
+
availableVersion: latestVersion,
|
|
3833
|
+
updateAvailable: false,
|
|
3834
|
+
humanHint: `Already on latest beta: v${currentVersion}`
|
|
3835
|
+
})
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
try {
|
|
3839
|
+
execSync3("npm install -g skillwiki@beta", {
|
|
3840
|
+
stdio: "pipe",
|
|
3841
|
+
timeout: 6e4
|
|
3842
|
+
});
|
|
3843
|
+
} catch (e) {
|
|
3844
|
+
return {
|
|
3845
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3846
|
+
result: err("INSTALL_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
return {
|
|
3850
|
+
exitCode: ExitCode.OK,
|
|
3851
|
+
result: ok({
|
|
3852
|
+
source: "npm",
|
|
3853
|
+
currentVersion,
|
|
3854
|
+
availableVersion: latestVersion,
|
|
3855
|
+
updateAvailable: true,
|
|
3856
|
+
newVersion: latestVersion,
|
|
3857
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latestVersion} via npm@beta`
|
|
2603
3858
|
})
|
|
2604
3859
|
};
|
|
2605
3860
|
}
|
|
2606
3861
|
|
|
2607
3862
|
// src/commands/transcripts.ts
|
|
2608
|
-
import { readdir as
|
|
2609
|
-
import { join as
|
|
3863
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile16 } from "fs/promises";
|
|
3864
|
+
import { join as join25 } from "path";
|
|
2610
3865
|
async function runTranscripts(input) {
|
|
2611
|
-
const dir =
|
|
3866
|
+
const dir = join25(input.vault, "raw", "transcripts");
|
|
2612
3867
|
let entries;
|
|
2613
3868
|
try {
|
|
2614
|
-
entries = await
|
|
3869
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
2615
3870
|
} catch {
|
|
2616
3871
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2617
3872
|
}
|
|
2618
3873
|
const transcripts = [];
|
|
2619
3874
|
for (const entry of entries) {
|
|
2620
3875
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2621
|
-
const filePath =
|
|
2622
|
-
const content = await
|
|
3876
|
+
const filePath = join25(dir, entry.name);
|
|
3877
|
+
const content = await readFile16(filePath, "utf8");
|
|
2623
3878
|
const fm = extractFrontmatter(content);
|
|
2624
3879
|
if (!fm.ok) continue;
|
|
2625
3880
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
@@ -2635,53 +3890,1943 @@ async function runTranscripts(input) {
|
|
|
2635
3890
|
return { exitCode: ExitCode.OK, result: ok({ transcripts, humanHint: hint }) };
|
|
2636
3891
|
}
|
|
2637
3892
|
|
|
2638
|
-
// src/
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
3893
|
+
// src/commands/project-index.ts
|
|
3894
|
+
import { readdir as readdir6, readFile as readFile17, writeFile as writeFile12, mkdir as mkdir8 } from "fs/promises";
|
|
3895
|
+
import { join as join26, dirname as dirname10 } from "path";
|
|
3896
|
+
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
3897
|
+
async function runProjectIndex(input) {
|
|
3898
|
+
const slug = input.slug;
|
|
3899
|
+
const projectDir = join26(input.vault, "projects", slug);
|
|
3900
|
+
try {
|
|
3901
|
+
await readdir6(projectDir);
|
|
3902
|
+
} catch {
|
|
3903
|
+
return {
|
|
3904
|
+
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
3905
|
+
result: err("PROJECT_NOT_FOUND", { slug, path: projectDir })
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
const wikilinkPattern = `[[${slug}]]`;
|
|
3909
|
+
const entries = [];
|
|
3910
|
+
const compoundDir = join26(input.vault, "projects", slug, "compound");
|
|
3911
|
+
try {
|
|
3912
|
+
const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
|
|
3913
|
+
for (const entry of compoundFiles) {
|
|
3914
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
3915
|
+
const filePath = join26(compoundDir, entry.name);
|
|
3916
|
+
let text;
|
|
3917
|
+
try {
|
|
3918
|
+
text = await readFile17(filePath, "utf8");
|
|
3919
|
+
} catch {
|
|
3920
|
+
continue;
|
|
3921
|
+
}
|
|
3922
|
+
const fm = extractFrontmatter(text);
|
|
3923
|
+
if (!fm.ok) continue;
|
|
3924
|
+
entries.push({
|
|
3925
|
+
page: `projects/${slug}/compound/${entry.name}`,
|
|
3926
|
+
type: typeof fm.data.type === "string" ? fm.data.type : "compound",
|
|
3927
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
} catch {
|
|
3931
|
+
}
|
|
3932
|
+
for (const dir of LAYER2_DIRS) {
|
|
3933
|
+
let files;
|
|
3934
|
+
try {
|
|
3935
|
+
files = await readdir6(join26(input.vault, dir), { withFileTypes: true });
|
|
3936
|
+
} catch {
|
|
3937
|
+
continue;
|
|
3938
|
+
}
|
|
3939
|
+
for (const entry of files) {
|
|
3940
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
3941
|
+
const filePath = join26(input.vault, dir, entry.name);
|
|
3942
|
+
let text;
|
|
3943
|
+
try {
|
|
3944
|
+
text = await readFile17(filePath, "utf8");
|
|
3945
|
+
} catch {
|
|
3946
|
+
continue;
|
|
3947
|
+
}
|
|
3948
|
+
const fm = extractFrontmatter(text);
|
|
3949
|
+
if (!fm.ok) continue;
|
|
3950
|
+
const pp = fm.data.provenance_projects;
|
|
3951
|
+
if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
|
|
3952
|
+
entries.push({
|
|
3953
|
+
page: `${dir}/${entry.name}`,
|
|
3954
|
+
type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
|
|
3955
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
3956
|
+
});
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
const typeOrder = { entity: 0, concept: 1, comparison: 2, query: 3, summary: 4, meta: 5, pattern: 6, gotcha: 7, lesson: 8, antipattern: 9, compound: 10 };
|
|
3960
|
+
entries.sort((a, b) => {
|
|
3961
|
+
const ta = typeOrder[a.type] ?? 99;
|
|
3962
|
+
const tb = typeOrder[b.type] ?? 99;
|
|
3963
|
+
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
3964
|
+
});
|
|
3965
|
+
const indexPath = join26(projectDir, "knowledge.md");
|
|
3966
|
+
let existing = false;
|
|
3967
|
+
let stale = false;
|
|
3968
|
+
try {
|
|
3969
|
+
const existingText = await readFile17(indexPath, "utf8");
|
|
3970
|
+
existing = true;
|
|
3971
|
+
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
3972
|
+
const existingPages = new Set(existingEntries.map((l) => {
|
|
3973
|
+
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
3974
|
+
return m ? m[1] : "";
|
|
3975
|
+
}));
|
|
3976
|
+
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
3977
|
+
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
3978
|
+
} catch {
|
|
3979
|
+
}
|
|
3980
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3981
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3982
|
+
for (const e of entries) {
|
|
3983
|
+
const group = e.type;
|
|
3984
|
+
if (!grouped.has(group)) grouped.set(group, []);
|
|
3985
|
+
grouped.get(group).push(e);
|
|
3986
|
+
}
|
|
3987
|
+
let body = `# Knowledge Index: ${slug}
|
|
3988
|
+
|
|
3989
|
+
Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
3990
|
+
|
|
3991
|
+
`;
|
|
3992
|
+
for (const [type, items] of grouped) {
|
|
3993
|
+
body += `## ${type}
|
|
3994
|
+
|
|
3995
|
+
`;
|
|
3996
|
+
for (const item of items) {
|
|
3997
|
+
const pageRef = item.page.replace(/\.md$/, "");
|
|
3998
|
+
body += `- [[${pageRef}]] \u2014 ${item.title}
|
|
3999
|
+
`;
|
|
4000
|
+
}
|
|
4001
|
+
body += "\n";
|
|
4002
|
+
}
|
|
4003
|
+
if (entries.length === 0) {
|
|
4004
|
+
body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
|
|
4005
|
+
`;
|
|
4006
|
+
}
|
|
4007
|
+
if (input.apply) {
|
|
4008
|
+
try {
|
|
4009
|
+
await mkdir8(dirname10(indexPath), { recursive: true });
|
|
4010
|
+
await writeFile12(indexPath, body, "utf8");
|
|
4011
|
+
} catch (e) {
|
|
4012
|
+
return {
|
|
4013
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4014
|
+
result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
|
|
4019
|
+
const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
|
|
4020
|
+
return {
|
|
4021
|
+
exitCode: ExitCode.OK,
|
|
4022
|
+
result: ok({
|
|
4023
|
+
slug,
|
|
4024
|
+
entries,
|
|
4025
|
+
existing,
|
|
4026
|
+
stale,
|
|
4027
|
+
index_path: `projects/${slug}/knowledge.md`,
|
|
4028
|
+
humanHint: `project: ${slug}
|
|
4029
|
+
entries: ${entries.length}${staleHint}
|
|
4030
|
+
${action}
|
|
4031
|
+
|
|
4032
|
+
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
4033
|
+
})
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
// src/commands/compound.ts
|
|
4038
|
+
import { writeFile as writeFile13, mkdir as mkdir9, readdir as readdir7, unlink as unlink2 } from "fs/promises";
|
|
4039
|
+
import { join as join27 } from "path";
|
|
4040
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4041
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
4042
|
+
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
4043
|
+
var FIELD_RE = {
|
|
4044
|
+
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
4045
|
+
friction: /^-\s+\*?\*?Friction:?\*?\*?\s*(.+)$/m,
|
|
4046
|
+
generalize: /^-\s+\*?\*?Generalize\?:?\*?\*?\s*(.+)$/m
|
|
4047
|
+
};
|
|
4048
|
+
function slugify(name) {
|
|
4049
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+$/g, "");
|
|
4050
|
+
}
|
|
4051
|
+
function inferType(improve, friction) {
|
|
4052
|
+
if (/\bshould\b/i.test(improve)) return "pattern";
|
|
4053
|
+
if (/\bbug\b|\berror\b/i.test(friction)) return "gotcha";
|
|
4054
|
+
return "lesson";
|
|
4055
|
+
}
|
|
4056
|
+
function extractTags(generalize) {
|
|
4057
|
+
const tags = [];
|
|
4058
|
+
const parenRe = /\(([^)]+)\)/g;
|
|
4059
|
+
let match;
|
|
4060
|
+
while ((match = parenRe.exec(generalize)) !== null) {
|
|
4061
|
+
const words = match[1].trim().split(/\s+/);
|
|
4062
|
+
for (const w of words) {
|
|
4063
|
+
const cleaned = w.toLowerCase().replace(/[^a-z0-9-]/g, "").trim();
|
|
4064
|
+
if (cleaned.length > 0) tags.push(cleaned);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
const appliesRe = /applies to any\s+(.+?)(?:\.|,|$)/i;
|
|
4068
|
+
const appliesMatch = generalize.match(appliesRe);
|
|
4069
|
+
if (appliesMatch) {
|
|
4070
|
+
const words = appliesMatch[1].trim().split(/\s+/);
|
|
4071
|
+
for (const w of words) {
|
|
4072
|
+
const cleaned = w.toLowerCase().replace(/[^a-z0-9-]/g, "").trim();
|
|
4073
|
+
if (cleaned.length > 0) tags.push(cleaned);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
if (tags.length === 0) {
|
|
4077
|
+
tags.push("dev-loop");
|
|
4078
|
+
}
|
|
4079
|
+
return [...new Set(tags)];
|
|
4080
|
+
}
|
|
4081
|
+
function parseRationale(generalize) {
|
|
4082
|
+
const yesMatch = generalize.match(/^yes[,:]\s*(.+)$/i);
|
|
4083
|
+
if (yesMatch) return yesMatch[1].trim();
|
|
4084
|
+
if (/^yes$/i.test(generalize.trim())) return "";
|
|
4085
|
+
return generalize.trim();
|
|
4086
|
+
}
|
|
4087
|
+
function parseRetroEntries(logText) {
|
|
4088
|
+
const entries = [];
|
|
4089
|
+
const lines = logText.split("\n");
|
|
4090
|
+
let currentDate = "";
|
|
4091
|
+
let currentCycleName = "";
|
|
4092
|
+
let currentBlock = [];
|
|
4093
|
+
let foundHeading = false;
|
|
4094
|
+
for (const line of lines) {
|
|
4095
|
+
const headingMatch = line.match(RETRO_HEADING_RE);
|
|
4096
|
+
if (headingMatch) {
|
|
4097
|
+
if (foundHeading && currentBlock.length > 0) {
|
|
4098
|
+
const entry = extractRetroFields(currentDate, currentCycleName, currentBlock);
|
|
4099
|
+
if (entry) entries.push(entry);
|
|
4100
|
+
}
|
|
4101
|
+
currentDate = headingMatch[1];
|
|
4102
|
+
currentCycleName = headingMatch[3];
|
|
4103
|
+
currentBlock = [];
|
|
4104
|
+
foundHeading = true;
|
|
4105
|
+
continue;
|
|
4106
|
+
}
|
|
4107
|
+
if (foundHeading && /^## /.test(line)) {
|
|
4108
|
+
const entry = extractRetroFields(currentDate, currentCycleName, currentBlock);
|
|
4109
|
+
if (entry) entries.push(entry);
|
|
4110
|
+
foundHeading = false;
|
|
4111
|
+
currentBlock = [];
|
|
4112
|
+
continue;
|
|
4113
|
+
}
|
|
4114
|
+
if (foundHeading) {
|
|
4115
|
+
currentBlock.push(line);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
if (foundHeading && currentBlock.length > 0) {
|
|
4119
|
+
const entry = extractRetroFields(currentDate, currentCycleName, currentBlock);
|
|
4120
|
+
if (entry) entries.push(entry);
|
|
4121
|
+
}
|
|
4122
|
+
return entries;
|
|
4123
|
+
}
|
|
4124
|
+
function extractRetroFields(date, cycleName, block) {
|
|
4125
|
+
const text = block.join("\n");
|
|
4126
|
+
const improveMatch = text.match(FIELD_RE.improve);
|
|
4127
|
+
const frictionMatch = text.match(FIELD_RE.friction);
|
|
4128
|
+
const generalizeMatch = text.match(FIELD_RE.generalize);
|
|
4129
|
+
if (!generalizeMatch) return null;
|
|
4130
|
+
return {
|
|
4131
|
+
date,
|
|
4132
|
+
cycleName,
|
|
4133
|
+
improve: improveMatch?.[1]?.trim() ?? "",
|
|
4134
|
+
friction: frictionMatch?.[1]?.trim() ?? "",
|
|
4135
|
+
generalize: generalizeMatch[1].trim()
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
async function runCompound(input) {
|
|
4139
|
+
const logPath = join27(input.vault, "log.md");
|
|
4140
|
+
let logText;
|
|
4141
|
+
try {
|
|
4142
|
+
logText = await readFile18(logPath, "utf8");
|
|
4143
|
+
} catch {
|
|
4144
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
4145
|
+
}
|
|
4146
|
+
const entries = parseRetroEntries(logText);
|
|
4147
|
+
const promoted = [];
|
|
4148
|
+
const skipped = [];
|
|
4149
|
+
const compoundDir = join27(input.vault, "projects", input.project, "compound");
|
|
4150
|
+
for (const entry of entries) {
|
|
4151
|
+
const generalizeValue = entry.generalize.trim();
|
|
4152
|
+
if (!/^yes/i.test(generalizeValue)) {
|
|
4153
|
+
skipped.push(entry.date);
|
|
4154
|
+
continue;
|
|
4155
|
+
}
|
|
4156
|
+
const slug = slugify(entry.cycleName);
|
|
4157
|
+
const compoundPath = join27(compoundDir, `${slug}.md`);
|
|
4158
|
+
if (existsSync7(compoundPath)) {
|
|
4159
|
+
skipped.push(entry.date);
|
|
4160
|
+
continue;
|
|
4161
|
+
}
|
|
4162
|
+
const type = inferType(entry.improve, entry.friction);
|
|
4163
|
+
const rationale = parseRationale(generalizeValue);
|
|
4164
|
+
const tags = extractTags(generalizeValue);
|
|
4165
|
+
const tagsYaml = tags.map((t) => t).join(", ");
|
|
4166
|
+
const title = entry.cycleName;
|
|
4167
|
+
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
|
|
4168
|
+
const frontmatter = [
|
|
4169
|
+
"---",
|
|
4170
|
+
`title: ${title}`,
|
|
4171
|
+
`created: ${entry.date}`,
|
|
4172
|
+
`updated: ${entry.date}`,
|
|
4173
|
+
`type: ${type}`,
|
|
4174
|
+
`tags: [${tagsYaml}]`,
|
|
4175
|
+
`confidence: medium`,
|
|
4176
|
+
`project: "[[${input.project}]]"`,
|
|
4177
|
+
`work_items: []`,
|
|
4178
|
+
"---"
|
|
4179
|
+
].join("\n");
|
|
4180
|
+
const body = [
|
|
4181
|
+
`## ${typeLabel}`,
|
|
4182
|
+
"",
|
|
4183
|
+
entry.improve,
|
|
4184
|
+
"",
|
|
4185
|
+
"## Evidence",
|
|
4186
|
+
"",
|
|
4187
|
+
entry.friction,
|
|
4188
|
+
"",
|
|
4189
|
+
"## Source",
|
|
4190
|
+
"",
|
|
4191
|
+
`Retro from ${entry.date} | ${entry.cycleName}. Generalize rationale: ${rationale}`,
|
|
4192
|
+
""
|
|
4193
|
+
].join("\n");
|
|
4194
|
+
const content = frontmatter + "\n" + body;
|
|
4195
|
+
if (!input.dryRun) {
|
|
4196
|
+
if (!existsSync7(compoundDir)) {
|
|
4197
|
+
await mkdir9(compoundDir, { recursive: true });
|
|
4198
|
+
}
|
|
4199
|
+
await writeFile13(compoundPath, content, "utf8");
|
|
4200
|
+
}
|
|
4201
|
+
promoted.push(`${slug}.md`);
|
|
4202
|
+
}
|
|
4203
|
+
const exitCode = promoted.length > 0 ? ExitCode.COMPOUND_PROMOTED : ExitCode.OK;
|
|
4204
|
+
const hintLines = [`scanned: ${entries.length}`];
|
|
4205
|
+
if (promoted.length > 0) hintLines.push(`promoted: ${promoted.length}`);
|
|
4206
|
+
if (skipped.length > 0) hintLines.push(`skipped (Generalize?: no): ${skipped.length}`);
|
|
4207
|
+
return {
|
|
4208
|
+
exitCode,
|
|
4209
|
+
result: ok({
|
|
4210
|
+
scanned: entries.length,
|
|
4211
|
+
promoted,
|
|
4212
|
+
skipped,
|
|
4213
|
+
humanHint: hintLines.join("\n")
|
|
4214
|
+
})
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
async function runCompoundDelete(input) {
|
|
4218
|
+
const projectDir = join27(input.vault, "projects", input.project);
|
|
4219
|
+
if (!existsSync7(projectDir)) {
|
|
4220
|
+
return {
|
|
4221
|
+
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
4222
|
+
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
const entryName = input.entry.replace(/\.md$/, "");
|
|
4226
|
+
const compoundPath = join27(projectDir, "compound", `${entryName}.md`);
|
|
4227
|
+
if (!existsSync7(compoundPath)) {
|
|
4228
|
+
return {
|
|
4229
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
4230
|
+
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
try {
|
|
4234
|
+
await unlink2(compoundPath);
|
|
4235
|
+
} catch (e) {
|
|
4236
|
+
return {
|
|
4237
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4238
|
+
result: err("WRITE_FAILED", { file: compoundPath, message: String(e) })
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
const indexResult = await runProjectIndex({ vault: input.vault, slug: input.project, apply: true });
|
|
4242
|
+
if (!indexResult.result.ok) {
|
|
4243
|
+
return {
|
|
4244
|
+
exitCode: indexResult.exitCode,
|
|
4245
|
+
result: err("INDEX_REGEN_FAILED", { detail: indexResult.result })
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
return {
|
|
4249
|
+
exitCode: ExitCode.OK,
|
|
4250
|
+
result: ok({
|
|
4251
|
+
deleted: compoundPath,
|
|
4252
|
+
project: input.project,
|
|
4253
|
+
humanHint: `deleted: ${entryName}.md
|
|
4254
|
+
project: ${input.project}
|
|
4255
|
+
knowledge.md regenerated`
|
|
4256
|
+
})
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
async function runCompoundList(input) {
|
|
4260
|
+
const compoundDir = join27(input.vault, "projects", input.project, "compound");
|
|
4261
|
+
if (!existsSync7(compoundDir)) {
|
|
4262
|
+
return {
|
|
4263
|
+
exitCode: ExitCode.OK,
|
|
4264
|
+
result: ok({
|
|
4265
|
+
project: input.project,
|
|
4266
|
+
entries: [],
|
|
4267
|
+
humanHint: `project: ${input.project}
|
|
4268
|
+
entries: 0
|
|
4269
|
+
no compound directory found`
|
|
4270
|
+
})
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
let dirents;
|
|
4274
|
+
try {
|
|
4275
|
+
dirents = await readdir7(compoundDir, { withFileTypes: true });
|
|
4276
|
+
} catch {
|
|
4277
|
+
return {
|
|
4278
|
+
exitCode: ExitCode.OK,
|
|
4279
|
+
result: ok({
|
|
4280
|
+
project: input.project,
|
|
4281
|
+
entries: [],
|
|
4282
|
+
humanHint: `project: ${input.project}
|
|
4283
|
+
entries: 0
|
|
4284
|
+
could not read compound directory`
|
|
4285
|
+
})
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
const entries = [];
|
|
4289
|
+
for (const dirent of dirents) {
|
|
4290
|
+
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
4291
|
+
const filePath = join27(compoundDir, dirent.name);
|
|
4292
|
+
let text;
|
|
4293
|
+
try {
|
|
4294
|
+
text = await readFile18(filePath, "utf8");
|
|
4295
|
+
} catch {
|
|
4296
|
+
continue;
|
|
4297
|
+
}
|
|
4298
|
+
const fm = extractFrontmatter(text);
|
|
4299
|
+
if (!fm.ok) continue;
|
|
4300
|
+
const tags = Array.isArray(fm.data.tags) ? fm.data.tags.map((t) => String(t)) : typeof fm.data.tags === "string" ? fm.data.tags.split(",").map((s) => s.trim()) : [];
|
|
4301
|
+
entries.push({
|
|
4302
|
+
file: dirent.name,
|
|
4303
|
+
title: typeof fm.data.title === "string" ? fm.data.title : dirent.name.replace(/\.md$/, ""),
|
|
4304
|
+
type: typeof fm.data.type === "string" ? fm.data.type : "lesson",
|
|
4305
|
+
created: typeof fm.data.created === "string" ? fm.data.created : "",
|
|
4306
|
+
tags
|
|
4307
|
+
});
|
|
4308
|
+
}
|
|
4309
|
+
const hint = entries.length > 0 ? [`project: ${input.project}`, `entries: ${entries.length}`, "", ...entries.map((e) => ` ${e.file}: ${e.title} (${e.type}, created: ${e.created || "unknown"}, tags: ${e.tags.join(", ") || "none"})`)].join("\n") : `project: ${input.project}
|
|
4310
|
+
entries: 0
|
|
4311
|
+
no compound entries found`;
|
|
4312
|
+
return {
|
|
4313
|
+
exitCode: ExitCode.OK,
|
|
4314
|
+
result: ok({
|
|
4315
|
+
project: input.project,
|
|
4316
|
+
entries,
|
|
4317
|
+
humanHint: hint
|
|
4318
|
+
})
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
// src/commands/observe.ts
|
|
4323
|
+
import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
|
|
4324
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
4325
|
+
import { join as join28 } from "path";
|
|
4326
|
+
import { createHash as createHash3 } from "crypto";
|
|
4327
|
+
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
4328
|
+
function slugify2(text) {
|
|
4329
|
+
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4330
|
+
return words || "untitled";
|
|
4331
|
+
}
|
|
4332
|
+
async function runObserve(input) {
|
|
4333
|
+
const kind = input.kind || "note";
|
|
4334
|
+
if (!ALLOWED_KINDS.has(kind)) {
|
|
4335
|
+
return {
|
|
4336
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4337
|
+
result: err("SCHEME_REJECTED", {
|
|
4338
|
+
message: `Invalid kind "${kind}". Allowed: ${[...ALLOWED_KINDS].join(", ")}`
|
|
4339
|
+
})
|
|
4340
|
+
};
|
|
4341
|
+
}
|
|
4342
|
+
if (!input.text || input.text.trim().length === 0) {
|
|
4343
|
+
return {
|
|
4344
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4345
|
+
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
4348
|
+
if (!existsSync8(input.vault) || !statSync2(input.vault).isDirectory()) {
|
|
4349
|
+
return {
|
|
4350
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4351
|
+
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
const transcriptsDir = join28(input.vault, "raw", "transcripts");
|
|
4355
|
+
try {
|
|
4356
|
+
await mkdir10(transcriptsDir, { recursive: true });
|
|
4357
|
+
} catch {
|
|
4358
|
+
return {
|
|
4359
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4360
|
+
result: err("VAULT_PATH_INVALID", { path: transcriptsDir })
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4364
|
+
const slug = slugify2(input.text);
|
|
4365
|
+
const fileName = `${today}-observation-${slug}.md`;
|
|
4366
|
+
const filePath = join28(transcriptsDir, fileName);
|
|
4367
|
+
const body = `
|
|
4368
|
+
${input.text.trim()}
|
|
4369
|
+
`;
|
|
4370
|
+
const sha256 = createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
4371
|
+
const frontmatterLines = [
|
|
4372
|
+
"---",
|
|
4373
|
+
"source_url:",
|
|
4374
|
+
`ingested: ${today}`,
|
|
4375
|
+
`sha256: ${sha256}`,
|
|
4376
|
+
`kind: ${kind}`
|
|
4377
|
+
];
|
|
4378
|
+
if (input.project) {
|
|
4379
|
+
frontmatterLines.push(`project: "[[${input.project}]]"`);
|
|
4380
|
+
}
|
|
4381
|
+
frontmatterLines.push("---");
|
|
4382
|
+
const content = frontmatterLines.join("\n") + body;
|
|
4383
|
+
try {
|
|
4384
|
+
await writeFile14(filePath, content, "utf8");
|
|
4385
|
+
} catch (e) {
|
|
4386
|
+
return {
|
|
4387
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4388
|
+
result: err("WRITE_FAILED", { path: filePath, message: String(e) })
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
appendLastOp(input.vault, {
|
|
4392
|
+
operation: "observe",
|
|
4393
|
+
summary: `created observation: ${slug}`,
|
|
4394
|
+
files: [`raw/transcripts/${fileName}`],
|
|
4395
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4396
|
+
});
|
|
4397
|
+
const relPath = `raw/transcripts/${fileName}`;
|
|
4398
|
+
const humanHint = `created ${relPath} (${sha256.slice(0, 12)}...)`;
|
|
4399
|
+
return {
|
|
4400
|
+
exitCode: ExitCode.OK,
|
|
4401
|
+
result: ok({ path: relPath, sha256, humanHint })
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
// src/commands/ingest.ts
|
|
4406
|
+
import { readFile as readFile19, writeFile as writeFile15, mkdir as mkdir11 } from "fs/promises";
|
|
4407
|
+
import { join as join29 } from "path";
|
|
4408
|
+
import { createHash as createHash4 } from "crypto";
|
|
4409
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
4410
|
+
var TYPE_DIR = {
|
|
4411
|
+
entity: "entities",
|
|
4412
|
+
concept: "concepts",
|
|
4413
|
+
comparison: "comparisons",
|
|
4414
|
+
query: "queries"
|
|
4415
|
+
};
|
|
4416
|
+
var ALLOWED_PROVENANCE = /* @__PURE__ */ new Set(["research", "project"]);
|
|
4417
|
+
function slugify3(text) {
|
|
4418
|
+
return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "untitled";
|
|
4419
|
+
}
|
|
4420
|
+
function isUrl(source) {
|
|
4421
|
+
try {
|
|
4422
|
+
const u = new URL(source);
|
|
4423
|
+
return u.protocol === "https:" || u.protocol === "http:";
|
|
4424
|
+
} catch {
|
|
4425
|
+
return false;
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
function todayIso() {
|
|
4429
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4430
|
+
}
|
|
4431
|
+
function buildRawContent(sourceUrl, ingested, sha256, body) {
|
|
4432
|
+
const lines = [
|
|
4433
|
+
"---",
|
|
4434
|
+
sourceUrl !== null ? `source_url: "${sourceUrl}"` : "source_url:",
|
|
4435
|
+
`created: ${ingested}`,
|
|
4436
|
+
`ingested: ${ingested}`,
|
|
4437
|
+
`sha256: ${sha256}`,
|
|
4438
|
+
`ingested_by: wiki-ingest`,
|
|
4439
|
+
"---",
|
|
4440
|
+
"",
|
|
4441
|
+
body
|
|
4442
|
+
];
|
|
4443
|
+
return lines.join("\n");
|
|
4444
|
+
}
|
|
4445
|
+
function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance) {
|
|
4446
|
+
const aliases = [];
|
|
4447
|
+
const sourcesYaml = ` - ${rawRelPath}`;
|
|
4448
|
+
const tagsYaml = tags.length > 0 ? tags.map((t) => ` - ${t}`).join("\n") : " []";
|
|
4449
|
+
const fm = {
|
|
4450
|
+
title,
|
|
4451
|
+
aliases,
|
|
4452
|
+
created: ingested,
|
|
4453
|
+
updated: ingested,
|
|
4454
|
+
type,
|
|
4455
|
+
tags,
|
|
4456
|
+
sources: [rawRelPath],
|
|
4457
|
+
confidence: "medium"
|
|
4458
|
+
};
|
|
4459
|
+
if (provenance) {
|
|
4460
|
+
fm.provenance = provenance;
|
|
4461
|
+
}
|
|
4462
|
+
const fmLines = ["---"];
|
|
4463
|
+
fmLines.push(`title: "${title}"`);
|
|
4464
|
+
if (aliases.length > 0) {
|
|
4465
|
+
fmLines.push("aliases:");
|
|
4466
|
+
for (const a of aliases) fmLines.push(` - ${a}`);
|
|
4467
|
+
} else {
|
|
4468
|
+
fmLines.push("aliases: []");
|
|
4469
|
+
}
|
|
4470
|
+
fmLines.push(`created: ${ingested}`);
|
|
4471
|
+
fmLines.push(`updated: ${ingested}`);
|
|
4472
|
+
fmLines.push(`type: ${type}`);
|
|
4473
|
+
fmLines.push("tags:");
|
|
4474
|
+
fmLines.push(tagsYaml);
|
|
4475
|
+
fmLines.push("sources:");
|
|
4476
|
+
fmLines.push(sourcesYaml);
|
|
4477
|
+
fmLines.push("confidence: medium");
|
|
4478
|
+
if (provenance) {
|
|
4479
|
+
fmLines.push(`provenance: ${provenance}`);
|
|
4480
|
+
}
|
|
4481
|
+
fmLines.push("---");
|
|
4482
|
+
fmLines.push("");
|
|
4483
|
+
const body = [
|
|
4484
|
+
`# ${title}`,
|
|
4485
|
+
"",
|
|
4486
|
+
"## Overview",
|
|
4487
|
+
"",
|
|
4488
|
+
"## See also",
|
|
4489
|
+
"",
|
|
4490
|
+
"## Sources",
|
|
4491
|
+
"",
|
|
4492
|
+
`^[${rawRelPath}]`,
|
|
4493
|
+
""
|
|
4494
|
+
].join("\n");
|
|
4495
|
+
return fmLines.join("\n") + body;
|
|
4496
|
+
}
|
|
4497
|
+
async function runIngest(input) {
|
|
4498
|
+
if (!input.source || input.source.trim().length === 0) {
|
|
4499
|
+
return {
|
|
4500
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4501
|
+
result: err("SCHEME_REJECTED", { message: "source is required" })
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
if (!input.vault || input.vault.trim().length === 0) {
|
|
4505
|
+
return {
|
|
4506
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4507
|
+
result: err("VAULT_PATH_INVALID", { message: "vault path is required" })
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
if (!input.type || !ALLOWED_TYPES.has(input.type)) {
|
|
4511
|
+
return {
|
|
4512
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4513
|
+
result: err("SCHEME_REJECTED", {
|
|
4514
|
+
message: `Invalid type "${input.type}". Allowed: ${[...ALLOWED_TYPES].join(", ")}`
|
|
4515
|
+
})
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
if (!input.title || input.title.trim().length === 0) {
|
|
4519
|
+
return {
|
|
4520
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4521
|
+
result: err("SCHEME_REJECTED", { message: "title is required" })
|
|
4522
|
+
};
|
|
4523
|
+
}
|
|
4524
|
+
if (input.provenance && !ALLOWED_PROVENANCE.has(input.provenance)) {
|
|
4525
|
+
return {
|
|
4526
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4527
|
+
result: err("SCHEME_REJECTED", {
|
|
4528
|
+
message: `Invalid provenance "${input.provenance}". Allowed: ${[...ALLOWED_PROVENANCE].join(", ")}`
|
|
4529
|
+
})
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
let sourceContent;
|
|
4533
|
+
let sourceUrl = null;
|
|
4534
|
+
if (isUrl(input.source)) {
|
|
4535
|
+
sourceUrl = input.source;
|
|
4536
|
+
const guardResult = runFetchGuardSync({ url: input.source });
|
|
4537
|
+
if (!guardResult.result.ok) {
|
|
4538
|
+
return {
|
|
4539
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4540
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4541
|
+
message: "source URL blocked by fetch-guard",
|
|
4542
|
+
guardError: guardResult.result.error,
|
|
4543
|
+
guardDetail: guardResult.result.detail
|
|
4544
|
+
})
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
const fetchResult = await controlledFetch(input.source, {
|
|
4548
|
+
timeoutMs: 15e3,
|
|
4549
|
+
maxBytes: 1024 * 1024,
|
|
4550
|
+
// 1 MB
|
|
4551
|
+
maxRedirects: 5
|
|
4552
|
+
});
|
|
4553
|
+
if (!fetchResult.ok) {
|
|
4554
|
+
return {
|
|
4555
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4556
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4557
|
+
message: "failed to fetch source URL",
|
|
4558
|
+
fetchError: fetchResult.error,
|
|
4559
|
+
fetchDetail: fetchResult.detail
|
|
4560
|
+
})
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
sourceContent = fetchResult.data.body;
|
|
4564
|
+
} else {
|
|
4565
|
+
try {
|
|
4566
|
+
sourceContent = await readFile19(input.source, "utf8");
|
|
4567
|
+
} catch {
|
|
4568
|
+
return {
|
|
4569
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
4570
|
+
result: err("FILE_NOT_FOUND", { path: input.source })
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
|
|
4575
|
+
const today = todayIso();
|
|
4576
|
+
const slug = slugify3(input.title);
|
|
4577
|
+
const tags = input.tags && input.tags.length > 0 ? input.tags : [];
|
|
4578
|
+
const rawRelPath = `raw/articles/${slug}.md`;
|
|
4579
|
+
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
4580
|
+
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
4581
|
+
const rawAbsPath = join29(input.vault, rawRelPath);
|
|
4582
|
+
const typedAbsPath = join29(input.vault, typedRelPath);
|
|
4583
|
+
const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
|
|
4584
|
+
const typedContent = buildTypedContent(
|
|
4585
|
+
input.title,
|
|
4586
|
+
today,
|
|
4587
|
+
input.type,
|
|
4588
|
+
tags,
|
|
4589
|
+
rawRelPath,
|
|
4590
|
+
input.provenance
|
|
4591
|
+
);
|
|
4592
|
+
if (input.dryRun) {
|
|
4593
|
+
return {
|
|
4594
|
+
exitCode: ExitCode.OK,
|
|
4595
|
+
result: ok({
|
|
4596
|
+
raw_path: rawRelPath,
|
|
4597
|
+
typed_path: typedRelPath,
|
|
4598
|
+
sha256,
|
|
4599
|
+
dry_run: true,
|
|
4600
|
+
humanHint: [
|
|
4601
|
+
`DRY RUN \u2014 would create:`,
|
|
4602
|
+
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
4603
|
+
` ${typedRelPath}`,
|
|
4604
|
+
` type: ${input.type}, tags: [${tags.join(", ")}]`,
|
|
4605
|
+
input.provenance ? ` provenance: ${input.provenance}` : ""
|
|
4606
|
+
].filter(Boolean).join("\n")
|
|
4607
|
+
})
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
const typedFm = {
|
|
4611
|
+
title: input.title,
|
|
4612
|
+
aliases: [],
|
|
4613
|
+
created: today,
|
|
4614
|
+
updated: today,
|
|
4615
|
+
type: input.type,
|
|
4616
|
+
tags,
|
|
4617
|
+
sources: [rawRelPath],
|
|
4618
|
+
confidence: "medium",
|
|
4619
|
+
...input.provenance ? { provenance: input.provenance } : {}
|
|
4620
|
+
};
|
|
4621
|
+
const det = detectSchema(typedFm);
|
|
4622
|
+
if (!det.schema) {
|
|
4623
|
+
return {
|
|
4624
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4625
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4626
|
+
message: "generated typed-knowledge page could not be detected as a valid schema"
|
|
4627
|
+
})
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
const parsed = TypedKnowledgeSchema.safeParse(typedFm);
|
|
4631
|
+
if (!parsed.success) {
|
|
4632
|
+
const errors = parsed.error.issues.map((i) => ({
|
|
4633
|
+
path: i.path.join("."),
|
|
4634
|
+
message: i.message
|
|
4635
|
+
}));
|
|
4636
|
+
return {
|
|
4637
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4638
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4639
|
+
message: "generated typed-knowledge page failed schema validation",
|
|
4640
|
+
errors
|
|
4641
|
+
})
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
try {
|
|
4645
|
+
await mkdir11(join29(input.vault, "raw", "articles"), { recursive: true });
|
|
4646
|
+
await writeFile15(rawAbsPath, rawContent, "utf8");
|
|
4647
|
+
} catch (e) {
|
|
4648
|
+
return {
|
|
4649
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4650
|
+
result: err("WRITE_FAILED", { path: rawAbsPath, message: String(e) })
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
try {
|
|
4654
|
+
await mkdir11(join29(input.vault, typedDir), { recursive: true });
|
|
4655
|
+
await writeFile15(typedAbsPath, typedContent, "utf8");
|
|
4656
|
+
} catch (e) {
|
|
4657
|
+
return {
|
|
4658
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4659
|
+
result: err("WRITE_FAILED", { path: typedAbsPath, message: String(e) })
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
const humanHint = [
|
|
4663
|
+
`created:`,
|
|
4664
|
+
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
4665
|
+
` ${typedRelPath}`
|
|
4666
|
+
].join("\n");
|
|
4667
|
+
appendLastOp(input.vault, {
|
|
4668
|
+
operation: "ingest",
|
|
4669
|
+
summary: `added ${slug}`,
|
|
4670
|
+
files: [rawRelPath, typedRelPath],
|
|
4671
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4672
|
+
});
|
|
4673
|
+
return {
|
|
4674
|
+
exitCode: ExitCode.OK,
|
|
4675
|
+
result: ok({
|
|
4676
|
+
raw_path: rawRelPath,
|
|
4677
|
+
typed_path: typedRelPath,
|
|
4678
|
+
sha256,
|
|
4679
|
+
dry_run: false,
|
|
4680
|
+
humanHint
|
|
4681
|
+
})
|
|
4682
|
+
};
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
// src/commands/tag-sync.ts
|
|
4686
|
+
import { writeFile as writeFile16 } from "fs/promises";
|
|
4687
|
+
var ENUM_MIRRORS = {
|
|
4688
|
+
provenance: ["research", "project", "mixed"],
|
|
4689
|
+
confidence: ["high", "medium", "low"]
|
|
4690
|
+
};
|
|
4691
|
+
function toNestedTag(field, value) {
|
|
4692
|
+
return `${field}/${value}`;
|
|
4693
|
+
}
|
|
4694
|
+
function expectedNestedTags(fm) {
|
|
4695
|
+
const expected = /* @__PURE__ */ new Set();
|
|
4696
|
+
for (const [field, allowedValues] of Object.entries(ENUM_MIRRORS)) {
|
|
4697
|
+
const value = fm[field];
|
|
4698
|
+
if (typeof value === "string" && allowedValues.includes(value)) {
|
|
4699
|
+
expected.add(toNestedTag(field, value));
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return expected;
|
|
4703
|
+
}
|
|
4704
|
+
function parseTagsFromYaml(rawFm) {
|
|
4705
|
+
const inlineMatch = rawFm.match(/^tags:\s*\[([^\]]*)\]/m);
|
|
4706
|
+
if (inlineMatch) {
|
|
4707
|
+
return inlineMatch[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter((t) => t.length > 0);
|
|
4708
|
+
}
|
|
4709
|
+
const lines = rawFm.split("\n");
|
|
4710
|
+
const tagItems = [];
|
|
4711
|
+
let inTags = false;
|
|
4712
|
+
for (const line of lines) {
|
|
4713
|
+
if (/^tags:\s*$/.test(line)) {
|
|
4714
|
+
inTags = true;
|
|
4715
|
+
continue;
|
|
4716
|
+
}
|
|
4717
|
+
if (inTags) {
|
|
4718
|
+
if (/^\s+-\s+/.test(line) && !/^\s+-\s+\[\[/.test(line)) {
|
|
4719
|
+
const value = line.replace(/^\s+-\s+/, "").trim().replace(/^['"]|['"]$/g, "");
|
|
4720
|
+
if (value.length > 0) tagItems.push(value);
|
|
4721
|
+
} else {
|
|
4722
|
+
break;
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
return tagItems;
|
|
4727
|
+
}
|
|
4728
|
+
function rebuildTagsSection(rawFm, existingTags, toAdd) {
|
|
4729
|
+
const allTags = [...existingTags, ...toAdd];
|
|
4730
|
+
const tagsLine = `tags: [${allTags.join(", ")}]`;
|
|
4731
|
+
if (/^tags:\s*\[/m.test(rawFm)) {
|
|
4732
|
+
return rawFm.replace(/^tags:\s*\[[^\]]*\]/m, tagsLine);
|
|
4733
|
+
}
|
|
4734
|
+
const lines = rawFm.split("\n");
|
|
4735
|
+
const out = [];
|
|
4736
|
+
let inTags = false;
|
|
4737
|
+
let tagsReplaced = false;
|
|
4738
|
+
for (const line of lines) {
|
|
4739
|
+
if (/^tags:\s*$/.test(line)) {
|
|
4740
|
+
inTags = true;
|
|
4741
|
+
if (!tagsReplaced) {
|
|
4742
|
+
out.push(tagsLine);
|
|
4743
|
+
tagsReplaced = true;
|
|
4744
|
+
}
|
|
4745
|
+
continue;
|
|
4746
|
+
}
|
|
4747
|
+
if (inTags) {
|
|
4748
|
+
if (/^\s+-\s+/.test(line) && !/^\s+-\s+\[\[/.test(line)) {
|
|
4749
|
+
continue;
|
|
4750
|
+
} else {
|
|
4751
|
+
inTags = false;
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
out.push(line);
|
|
4755
|
+
}
|
|
4756
|
+
if (!tagsReplaced) {
|
|
4757
|
+
out.push(tagsLine);
|
|
4758
|
+
}
|
|
4759
|
+
return out.join("\n");
|
|
4760
|
+
}
|
|
4761
|
+
async function runTagSync(input) {
|
|
4762
|
+
const scan = await scanVault(input.vault);
|
|
4763
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
4764
|
+
const synced = [];
|
|
4765
|
+
let unchanged = 0;
|
|
4766
|
+
for (const page of scan.data.typedKnowledge) {
|
|
4767
|
+
const text = await readPage(page);
|
|
4768
|
+
const split = splitFrontmatter(text);
|
|
4769
|
+
if (!split.ok) {
|
|
4770
|
+
unchanged++;
|
|
4771
|
+
continue;
|
|
4772
|
+
}
|
|
4773
|
+
const { rawFrontmatter, body } = split.data;
|
|
4774
|
+
const fm = {};
|
|
4775
|
+
for (const [field, allowedValues] of Object.entries(ENUM_MIRRORS)) {
|
|
4776
|
+
for (const v of allowedValues) {
|
|
4777
|
+
if (rawFrontmatter.includes(`${field}: ${v}`)) {
|
|
4778
|
+
fm[field] = v;
|
|
4779
|
+
break;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
const expected = expectedNestedTags(fm);
|
|
4784
|
+
if (expected.size === 0) {
|
|
4785
|
+
unchanged++;
|
|
4786
|
+
continue;
|
|
4787
|
+
}
|
|
4788
|
+
const existingTags = parseTagsFromYaml(rawFrontmatter);
|
|
4789
|
+
const existingSet = new Set(existingTags);
|
|
4790
|
+
const toAdd = [...expected].filter((t) => !existingSet.has(t));
|
|
4791
|
+
if (toAdd.length === 0) {
|
|
4792
|
+
unchanged++;
|
|
4793
|
+
continue;
|
|
4794
|
+
}
|
|
4795
|
+
const newFm = rebuildTagsSection(rawFrontmatter, existingTags, toAdd);
|
|
4796
|
+
const newText = `---
|
|
4797
|
+
${newFm}
|
|
4798
|
+
---
|
|
4799
|
+
${body}`;
|
|
4800
|
+
if (!input.dryRun) {
|
|
4801
|
+
await writeFile16(page.absPath, newText, "utf8");
|
|
4802
|
+
}
|
|
4803
|
+
synced.push(page.relPath);
|
|
4804
|
+
}
|
|
4805
|
+
if (!input.dryRun && synced.length > 0) {
|
|
4806
|
+
appendLastOp(input.vault, {
|
|
4807
|
+
operation: "tag-sync",
|
|
4808
|
+
summary: `synced tags on ${synced.length} pages`,
|
|
4809
|
+
files: synced,
|
|
4810
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
const exitCode = synced.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
4814
|
+
const hintLines = [`scanned: ${synced.length + unchanged}`];
|
|
4815
|
+
if (synced.length > 0) hintLines.push(`synced: ${synced.length}`);
|
|
4816
|
+
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
4817
|
+
if (input.dryRun && synced.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
4818
|
+
return {
|
|
4819
|
+
exitCode,
|
|
4820
|
+
result: ok({
|
|
4821
|
+
scanned: synced.length + unchanged,
|
|
4822
|
+
synced,
|
|
4823
|
+
unchanged,
|
|
4824
|
+
humanHint: hintLines.join("\n")
|
|
4825
|
+
})
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
// src/commands/sync.ts
|
|
4830
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4831
|
+
import { join as join30 } from "path";
|
|
4832
|
+
function runSyncStatus(input) {
|
|
4833
|
+
const vault = input.vault;
|
|
4834
|
+
if (!existsSync9(join30(vault, ".git"))) {
|
|
4835
|
+
return {
|
|
4836
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4837
|
+
result: ok({
|
|
4838
|
+
is_git_repo: false,
|
|
4839
|
+
dirty: 0,
|
|
4840
|
+
ahead: 0,
|
|
4841
|
+
behind: 0,
|
|
4842
|
+
last_commit: "never",
|
|
4843
|
+
status: "not_a_repo",
|
|
4844
|
+
humanHint: "not a git repository"
|
|
4845
|
+
})
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
4848
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
4849
|
+
const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
|
|
4850
|
+
const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
|
|
4851
|
+
let ahead = 0;
|
|
4852
|
+
let behind = 0;
|
|
4853
|
+
if (revOutput) {
|
|
4854
|
+
const parts = revOutput.split(/\s+/);
|
|
4855
|
+
behind = parseInt(parts[0], 10) || 0;
|
|
4856
|
+
ahead = parseInt(parts[1], 10) || 0;
|
|
4857
|
+
}
|
|
4858
|
+
const tsRaw = git(vault, ["log", "-1", "--format=%ct"]);
|
|
4859
|
+
let last_commit;
|
|
4860
|
+
if (tsRaw) {
|
|
4861
|
+
const ts = parseInt(tsRaw, 10);
|
|
4862
|
+
if (!isNaN(ts) && ts > 0) {
|
|
4863
|
+
last_commit = new Date(ts * 1e3).toISOString();
|
|
4864
|
+
} else {
|
|
4865
|
+
last_commit = "never";
|
|
4866
|
+
}
|
|
4867
|
+
} else {
|
|
4868
|
+
last_commit = "never";
|
|
4869
|
+
}
|
|
4870
|
+
let status;
|
|
4871
|
+
if (dirty > 0) {
|
|
4872
|
+
status = "dirty";
|
|
4873
|
+
} else if (ahead > 0) {
|
|
4874
|
+
status = "ahead";
|
|
4875
|
+
} else if (behind > 0) {
|
|
4876
|
+
status = "behind";
|
|
4877
|
+
} else {
|
|
4878
|
+
status = "clean";
|
|
4879
|
+
}
|
|
4880
|
+
const hintLines = [
|
|
4881
|
+
`status: ${status}`,
|
|
4882
|
+
`dirty: ${dirty}`,
|
|
4883
|
+
`ahead: ${ahead}`,
|
|
4884
|
+
`behind: ${behind}`,
|
|
4885
|
+
`last_commit: ${last_commit}`
|
|
4886
|
+
];
|
|
4887
|
+
const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
|
|
4888
|
+
return {
|
|
4889
|
+
exitCode,
|
|
4890
|
+
result: ok({
|
|
4891
|
+
is_git_repo: true,
|
|
4892
|
+
dirty,
|
|
4893
|
+
ahead,
|
|
4894
|
+
behind,
|
|
4895
|
+
last_commit,
|
|
4896
|
+
status,
|
|
4897
|
+
humanHint: hintLines.join("\n")
|
|
4898
|
+
})
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4901
|
+
async function runSyncPush(input) {
|
|
4902
|
+
const vault = input.vault;
|
|
4903
|
+
if (!existsSync9(join30(vault, ".git"))) {
|
|
4904
|
+
return {
|
|
4905
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4906
|
+
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4907
|
+
};
|
|
4908
|
+
}
|
|
4909
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
4910
|
+
const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
4911
|
+
if (dirtyFiles.length === 0) {
|
|
4912
|
+
return {
|
|
4913
|
+
exitCode: ExitCode.OK,
|
|
4914
|
+
result: ok({
|
|
4915
|
+
files_committed: 0,
|
|
4916
|
+
commit_message: "",
|
|
4917
|
+
pushed: false,
|
|
4918
|
+
humanHint: "nothing to commit, working tree clean"
|
|
4919
|
+
})
|
|
4920
|
+
};
|
|
4921
|
+
}
|
|
4922
|
+
const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
|
|
4923
|
+
if (lintResult.result.ok && lintResult.result.data.summary.errors > 0) {
|
|
4924
|
+
return {
|
|
4925
|
+
exitCode: ExitCode.LINT_HAS_ERRORS,
|
|
4926
|
+
result: err("LINT_ERRORS_BLOCK_PUSH", {
|
|
4927
|
+
errors: lintResult.result.data.summary.errors,
|
|
4928
|
+
buckets: lintResult.result.data.by_severity.error
|
|
4929
|
+
})
|
|
4930
|
+
};
|
|
4931
|
+
}
|
|
4932
|
+
try {
|
|
4933
|
+
gitStrict(vault, ["add", "-A"]);
|
|
4934
|
+
try {
|
|
4935
|
+
gitStrict(vault, ["reset", "HEAD", "--", ".skillwiki/last-op.json"]);
|
|
4936
|
+
} catch (_e) {
|
|
4937
|
+
}
|
|
4938
|
+
} catch (e) {
|
|
4939
|
+
return {
|
|
4940
|
+
exitCode: ExitCode.SYNC_PUSH_FAILED,
|
|
4941
|
+
result: err("GIT_ADD_FAILED", { message: String(e) })
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
const lastOps = readLastOp(vault);
|
|
4945
|
+
let commitMessage;
|
|
4946
|
+
if (lastOps.length > 0) {
|
|
4947
|
+
commitMessage = lastOps.map((op) => `${op.operation}: ${op.summary} (${op.files.length} files)`).join("; ");
|
|
4948
|
+
} else {
|
|
4949
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
4950
|
+
commitMessage = `sync: vault update ${timestamp}`;
|
|
4951
|
+
}
|
|
4952
|
+
try {
|
|
4953
|
+
gitStrict(vault, ["commit", "-m", commitMessage]);
|
|
4954
|
+
} catch (e) {
|
|
4955
|
+
return {
|
|
4956
|
+
exitCode: ExitCode.SYNC_PUSH_FAILED,
|
|
4957
|
+
result: err("GIT_COMMIT_FAILED", { message: String(e) })
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
clearLastOp(vault);
|
|
4961
|
+
let pushed = false;
|
|
4962
|
+
try {
|
|
4963
|
+
gitStrict(vault, ["push", "origin", "HEAD"]);
|
|
4964
|
+
pushed = true;
|
|
4965
|
+
} catch (e) {
|
|
4966
|
+
return {
|
|
4967
|
+
exitCode: ExitCode.SYNC_PUSH_FAILED,
|
|
4968
|
+
result: ok({
|
|
4969
|
+
files_committed: dirtyFiles.length,
|
|
4970
|
+
commit_message: commitMessage,
|
|
4971
|
+
pushed: false,
|
|
4972
|
+
humanHint: `committed ${dirtyFiles.length} file(s) but push failed: ${String(e)}`
|
|
4973
|
+
})
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
return {
|
|
4977
|
+
exitCode: ExitCode.OK,
|
|
4978
|
+
result: ok({
|
|
4979
|
+
files_committed: dirtyFiles.length,
|
|
4980
|
+
commit_message: commitMessage,
|
|
4981
|
+
pushed,
|
|
4982
|
+
humanHint: `committed and pushed ${dirtyFiles.length} file(s)`
|
|
4983
|
+
})
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
async function runSyncPull(input) {
|
|
4987
|
+
const vault = input.vault;
|
|
4988
|
+
if (!existsSync9(join30(vault, ".git"))) {
|
|
4989
|
+
return {
|
|
4990
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4991
|
+
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4992
|
+
};
|
|
4993
|
+
}
|
|
4994
|
+
let fetched = false;
|
|
4995
|
+
try {
|
|
4996
|
+
gitStrict(vault, ["fetch", "origin"]);
|
|
4997
|
+
fetched = true;
|
|
4998
|
+
} catch (e) {
|
|
4999
|
+
return {
|
|
5000
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
5001
|
+
result: err("GIT_FETCH_FAILED", { message: String(e) })
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
let pulled = false;
|
|
5005
|
+
let conflicts = 0;
|
|
5006
|
+
let filesUpdated = 0;
|
|
5007
|
+
try {
|
|
5008
|
+
const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
|
|
5009
|
+
pulled = true;
|
|
5010
|
+
const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
|
|
5011
|
+
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
5012
|
+
} catch (e) {
|
|
5013
|
+
const errString = String(e);
|
|
5014
|
+
if (errString.includes("conflict")) {
|
|
5015
|
+
const porcelain = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
5016
|
+
conflicts = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
|
|
5017
|
+
return {
|
|
5018
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
5019
|
+
result: ok({
|
|
5020
|
+
fetched,
|
|
5021
|
+
pulled: false,
|
|
5022
|
+
files_updated: 0,
|
|
5023
|
+
conflicts,
|
|
5024
|
+
lint_errors: 0,
|
|
5025
|
+
lint_warnings: 0,
|
|
5026
|
+
humanHint: `pull failed with ${conflicts} conflict(s) \u2014 resolve manually`
|
|
5027
|
+
})
|
|
5028
|
+
};
|
|
5029
|
+
}
|
|
5030
|
+
return {
|
|
5031
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
5032
|
+
result: err("GIT_PULL_FAILED", { message: errString })
|
|
5033
|
+
};
|
|
5034
|
+
}
|
|
5035
|
+
let lintErrors = 0;
|
|
5036
|
+
let lintWarnings = 0;
|
|
5037
|
+
const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
|
|
5038
|
+
if (lintResult.result.ok) {
|
|
5039
|
+
lintErrors = lintResult.result.data.summary.errors;
|
|
5040
|
+
lintWarnings = lintResult.result.data.summary.warnings;
|
|
5041
|
+
}
|
|
5042
|
+
const hintParts = [];
|
|
5043
|
+
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
5044
|
+
else hintParts.push("already up to date");
|
|
5045
|
+
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
5046
|
+
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
5047
|
+
const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
|
|
5048
|
+
return {
|
|
5049
|
+
exitCode,
|
|
5050
|
+
result: ok({
|
|
5051
|
+
fetched,
|
|
5052
|
+
pulled,
|
|
5053
|
+
files_updated: filesUpdated,
|
|
5054
|
+
conflicts,
|
|
5055
|
+
lint_errors: lintErrors,
|
|
5056
|
+
lint_warnings: lintWarnings,
|
|
5057
|
+
humanHint: hintParts.join(", ")
|
|
5058
|
+
})
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
|
|
5062
|
+
// src/commands/backup.ts
|
|
5063
|
+
import { statSync as statSync3, readdirSync as readdirSync2, readFileSync as readFileSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5064
|
+
import { join as join31, relative as relative3, dirname as dirname11 } from "path";
|
|
5065
|
+
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
5066
|
+
|
|
5067
|
+
// src/utils/s3-client.ts
|
|
5068
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
5069
|
+
function createS3Client(config) {
|
|
5070
|
+
const clientConfig = {
|
|
5071
|
+
endpoint: config.endpoint,
|
|
5072
|
+
region: config.region,
|
|
5073
|
+
credentials: {
|
|
5074
|
+
accessKeyId: config.accessKeyId,
|
|
5075
|
+
secretAccessKey: config.secretAccessKey
|
|
5076
|
+
},
|
|
5077
|
+
forcePathStyle: true
|
|
5078
|
+
// Required for SeaweedFS / MinIO
|
|
5079
|
+
};
|
|
5080
|
+
return new S3Client(clientConfig);
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
// src/commands/backup.ts
|
|
5084
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_modules", ".skillwiki"]);
|
|
5085
|
+
function* walkMarkdown(dir, base) {
|
|
5086
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
5087
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
5088
|
+
const full = join31(dir, entry.name);
|
|
5089
|
+
if (entry.isDirectory()) {
|
|
5090
|
+
yield* walkMarkdown(full, base);
|
|
5091
|
+
} else if (entry.name.endsWith(".md")) {
|
|
5092
|
+
yield relative3(base, full).replace(/\\/g, "/");
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
async function runBackupSync(input) {
|
|
5097
|
+
if (!input.accessKeyId || !input.secretAccessKey) {
|
|
5098
|
+
return {
|
|
5099
|
+
exitCode: ExitCode.BACKUP_SYNC_FAILED,
|
|
5100
|
+
result: err("BACKUP_SYNC_FAILED", {
|
|
5101
|
+
message: "Backup credentials not configured. Run: skillwiki config set BACKUP_ACCESS_KEY_ID <key>"
|
|
5102
|
+
})
|
|
5103
|
+
};
|
|
5104
|
+
}
|
|
5105
|
+
const client = createS3Client(input);
|
|
5106
|
+
let uploaded = 0;
|
|
5107
|
+
let skipped = 0;
|
|
5108
|
+
let failed = 0;
|
|
5109
|
+
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
5110
|
+
for (const relPath of files) {
|
|
5111
|
+
const absPath = join31(input.vault, relPath);
|
|
5112
|
+
const localStat = statSync3(absPath);
|
|
5113
|
+
let needsUpload = true;
|
|
5114
|
+
try {
|
|
5115
|
+
const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
|
|
5116
|
+
if (head.LastModified && head.LastModified >= localStat.mtime) {
|
|
5117
|
+
needsUpload = false;
|
|
5118
|
+
}
|
|
5119
|
+
} catch {
|
|
5120
|
+
}
|
|
5121
|
+
if (!needsUpload) {
|
|
5122
|
+
skipped++;
|
|
5123
|
+
continue;
|
|
5124
|
+
}
|
|
5125
|
+
if (input.dryRun) {
|
|
5126
|
+
uploaded++;
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
try {
|
|
5130
|
+
const body = readFileSync8(absPath);
|
|
5131
|
+
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
5132
|
+
uploaded++;
|
|
5133
|
+
} catch {
|
|
5134
|
+
failed++;
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
let pruned = 0;
|
|
5138
|
+
if (input.prune && !input.dryRun) {
|
|
5139
|
+
try {
|
|
5140
|
+
const localSet = new Set(files);
|
|
5141
|
+
const list = await client.send(new ListObjectsV2Command({ Bucket: input.bucket }));
|
|
5142
|
+
const toDelete = (list.Contents ?? []).filter((obj) => obj.Key && !localSet.has(obj.Key)).map((obj) => ({ Key: obj.Key }));
|
|
5143
|
+
if (toDelete.length > 0) {
|
|
5144
|
+
await client.send(new DeleteObjectsCommand({ Bucket: input.bucket, Delete: { Objects: toDelete } }));
|
|
5145
|
+
pruned = toDelete.length;
|
|
5146
|
+
}
|
|
5147
|
+
} catch {
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
const hintParts = [];
|
|
5151
|
+
if (input.dryRun) hintParts.push("DRY RUN \u2014");
|
|
5152
|
+
hintParts.push(`scanned: ${files.length}, uploaded: ${uploaded}, skipped: ${skipped}`);
|
|
5153
|
+
if (failed > 0) hintParts.push(`failed: ${failed}`);
|
|
5154
|
+
if (pruned > 0) hintParts.push(`pruned: ${pruned}`);
|
|
5155
|
+
return {
|
|
5156
|
+
exitCode: failed > 0 ? ExitCode.BACKUP_SYNC_FAILED : ExitCode.OK,
|
|
5157
|
+
result: ok({
|
|
5158
|
+
scanned: files.length,
|
|
5159
|
+
uploaded,
|
|
5160
|
+
skipped,
|
|
5161
|
+
failed,
|
|
5162
|
+
pruned,
|
|
5163
|
+
dry_run: input.dryRun ?? false,
|
|
5164
|
+
humanHint: hintParts.join(", ")
|
|
5165
|
+
})
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
async function runBackupRestore(input) {
|
|
5169
|
+
if (!input.accessKeyId || !input.secretAccessKey) {
|
|
5170
|
+
return {
|
|
5171
|
+
exitCode: ExitCode.BACKUP_SYNC_FAILED,
|
|
5172
|
+
result: err("BACKUP_SYNC_FAILED", {
|
|
5173
|
+
message: "Backup credentials not configured. Run: skillwiki config set BACKUP_ACCESS_KEY_ID <key>"
|
|
5174
|
+
})
|
|
5175
|
+
};
|
|
5176
|
+
}
|
|
5177
|
+
const client = createS3Client(input);
|
|
5178
|
+
const target = input.target ?? input.vault;
|
|
5179
|
+
let downloaded = 0;
|
|
5180
|
+
let skipped = 0;
|
|
5181
|
+
let conflicts = 0;
|
|
5182
|
+
try {
|
|
5183
|
+
const list = await client.send(new ListObjectsV2Command({ Bucket: input.bucket }));
|
|
5184
|
+
const objects = list.Contents ?? [];
|
|
5185
|
+
for (const obj of objects) {
|
|
5186
|
+
if (!obj.Key) continue;
|
|
5187
|
+
const localPath = join31(target, obj.Key);
|
|
5188
|
+
try {
|
|
5189
|
+
const localStat = statSync3(localPath);
|
|
5190
|
+
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
5191
|
+
conflicts++;
|
|
5192
|
+
continue;
|
|
5193
|
+
}
|
|
5194
|
+
} catch {
|
|
5195
|
+
}
|
|
5196
|
+
try {
|
|
5197
|
+
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
5198
|
+
const body = await resp.Body?.transformToByteArray();
|
|
5199
|
+
if (body) {
|
|
5200
|
+
mkdirSync3(dirname11(localPath), { recursive: true });
|
|
5201
|
+
writeFileSync4(localPath, Buffer.from(body));
|
|
5202
|
+
downloaded++;
|
|
5203
|
+
}
|
|
5204
|
+
} catch {
|
|
5205
|
+
skipped++;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
} catch (e) {
|
|
5209
|
+
return {
|
|
5210
|
+
exitCode: ExitCode.BACKUP_SYNC_FAILED,
|
|
5211
|
+
result: err("BACKUP_SYNC_FAILED", { message: `Failed to list bucket: ${String(e)}` })
|
|
5212
|
+
};
|
|
5213
|
+
}
|
|
5214
|
+
const hintParts = [`downloaded: ${downloaded}`];
|
|
5215
|
+
if (skipped > 0) hintParts.push(`skipped: ${skipped}`);
|
|
5216
|
+
if (conflicts > 0) hintParts.push(`conflicts: ${conflicts} (local is newer)`);
|
|
5217
|
+
if (downloaded > 0) {
|
|
5218
|
+
appendLastOp(target, {
|
|
5219
|
+
operation: "backup-restore",
|
|
5220
|
+
summary: `restored ${downloaded} files from S3`,
|
|
5221
|
+
files: [],
|
|
5222
|
+
// Don't enumerate potentially hundreds of files
|
|
5223
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5224
|
+
});
|
|
5225
|
+
}
|
|
5226
|
+
return {
|
|
5227
|
+
exitCode: conflicts > 0 ? ExitCode.BACKUP_RESTORE_CONFLICTS : ExitCode.OK,
|
|
5228
|
+
result: ok({ downloaded, skipped, conflicts, humanHint: hintParts.join(", ") })
|
|
5229
|
+
};
|
|
5230
|
+
}
|
|
5231
|
+
|
|
5232
|
+
// src/commands/status.ts
|
|
5233
|
+
import { existsSync as existsSync10, statSync as statSync4 } from "fs";
|
|
5234
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
5235
|
+
import { join as join32 } from "path";
|
|
5236
|
+
async function runStatus(input) {
|
|
5237
|
+
if (!existsSync10(input.vault)) {
|
|
5238
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
5239
|
+
}
|
|
5240
|
+
const scan = await scanVault(input.vault);
|
|
5241
|
+
if (!scan.ok) {
|
|
5242
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
5243
|
+
}
|
|
5244
|
+
const typedCounts = { entities: 0, concepts: 0, comparisons: 0, queries: 0, meta: 0 };
|
|
5245
|
+
for (const page of scan.data.typedKnowledge) {
|
|
5246
|
+
const segment = page.relPath.split("/")[0];
|
|
5247
|
+
if (segment in typedCounts) {
|
|
5248
|
+
typedCounts[segment]++;
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
let rawArticles = 0;
|
|
5252
|
+
let rawTranscripts = 0;
|
|
5253
|
+
for (const page of scan.data.raw) {
|
|
5254
|
+
const parts = page.relPath.split("/");
|
|
5255
|
+
if (parts[1] === "transcripts") rawTranscripts++;
|
|
5256
|
+
else rawArticles++;
|
|
5257
|
+
}
|
|
5258
|
+
const workItems = scan.data.workItems.length;
|
|
5259
|
+
const compound = scan.data.compound.length;
|
|
5260
|
+
let schemaVersion = "v1";
|
|
5261
|
+
try {
|
|
5262
|
+
const schemaContent = await readFile20(join32(input.vault, "SCHEMA.md"), "utf8");
|
|
5263
|
+
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
5264
|
+
if (versionMatch) schemaVersion = versionMatch[1];
|
|
5265
|
+
} catch {
|
|
5266
|
+
}
|
|
5267
|
+
const langResult = await resolveLang({ flag: void 0, envValue: input.langEnvValue, home: input.home });
|
|
5268
|
+
const allPages = [
|
|
5269
|
+
...scan.data.typedKnowledge,
|
|
5270
|
+
...scan.data.raw,
|
|
5271
|
+
...scan.data.workItems,
|
|
5272
|
+
...scan.data.compound
|
|
5273
|
+
];
|
|
5274
|
+
let lastModified = "";
|
|
5275
|
+
let maxTime = 0;
|
|
5276
|
+
for (const page of allPages) {
|
|
5277
|
+
try {
|
|
5278
|
+
const st = statSync4(page.absPath);
|
|
5279
|
+
if (st.mtimeMs > maxTime) {
|
|
5280
|
+
maxTime = st.mtimeMs;
|
|
5281
|
+
lastModified = st.mtime.toISOString();
|
|
5282
|
+
}
|
|
5283
|
+
} catch {
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
const pageCounts = {
|
|
5287
|
+
entities: typedCounts.entities,
|
|
5288
|
+
concepts: typedCounts.concepts,
|
|
5289
|
+
comparisons: typedCounts.comparisons,
|
|
5290
|
+
queries: typedCounts.queries,
|
|
5291
|
+
meta: typedCounts.meta,
|
|
5292
|
+
raw_articles: rawArticles,
|
|
5293
|
+
raw_transcripts: rawTranscripts,
|
|
5294
|
+
work_items: workItems,
|
|
5295
|
+
compound
|
|
5296
|
+
};
|
|
5297
|
+
const totalPages = Object.values(pageCounts).reduce((a, b) => a + b, 0);
|
|
5298
|
+
const rawTotal = rawArticles + rawTranscripts;
|
|
5299
|
+
const humanHint = [
|
|
5300
|
+
`vault: ${input.vault}`,
|
|
5301
|
+
`lang: ${langResult.value}`,
|
|
5302
|
+
`total: ${totalPages} pages`,
|
|
5303
|
+
` entities: ${pageCounts.entities} concepts: ${pageCounts.concepts} comparisons: ${pageCounts.comparisons} queries: ${pageCounts.queries} meta: ${pageCounts.meta}`,
|
|
5304
|
+
` raw: ${rawTotal} work_items: ${workItems} compound: ${compound}`,
|
|
5305
|
+
`last modified: ${lastModified.slice(0, 10)}`
|
|
5306
|
+
].join("\n");
|
|
5307
|
+
return {
|
|
5308
|
+
exitCode: ExitCode.OK,
|
|
5309
|
+
result: ok({
|
|
5310
|
+
vault_path: input.vault,
|
|
5311
|
+
schema_version: schemaVersion,
|
|
5312
|
+
lang: langResult.canonical,
|
|
5313
|
+
page_counts: pageCounts,
|
|
5314
|
+
total_pages: totalPages,
|
|
5315
|
+
last_modified: lastModified,
|
|
5316
|
+
humanHint
|
|
5317
|
+
})
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
// src/commands/seed.ts
|
|
5322
|
+
import { mkdir as mkdir12, writeFile as writeFile17, stat as stat7 } from "fs/promises";
|
|
5323
|
+
import { join as join33 } from "path";
|
|
5324
|
+
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5325
|
+
var EXAMPLE_PAGES = {
|
|
5326
|
+
"entities/example-project.md": `---
|
|
5327
|
+
title: Example Project
|
|
5328
|
+
aliases: [example-project]
|
|
5329
|
+
created: ${TODAY}
|
|
5330
|
+
updated: ${TODAY}
|
|
5331
|
+
type: entity
|
|
5332
|
+
tags: [research]
|
|
5333
|
+
sources: []
|
|
5334
|
+
confidence: medium
|
|
5335
|
+
provenance: research
|
|
5336
|
+
---
|
|
5337
|
+
|
|
5338
|
+
# Example Project
|
|
5339
|
+
|
|
5340
|
+
## Overview
|
|
5341
|
+
|
|
5342
|
+
This is a seed entity page demonstrating the typed-knowledge format. Replace it with a real entity from your research.
|
|
5343
|
+
|
|
5344
|
+
## Key Facts
|
|
5345
|
+
|
|
5346
|
+
- This vault was seeded on ${TODAY}
|
|
5347
|
+
- Entity pages describe people, organizations, products, or projects
|
|
5348
|
+
- Each page should cite sources from the \`raw/\` directory
|
|
5349
|
+
`,
|
|
5350
|
+
"concepts/example-concept.md": `---
|
|
5351
|
+
title: Example Concept
|
|
5352
|
+
aliases: [example-concept]
|
|
5353
|
+
created: ${TODAY}
|
|
5354
|
+
updated: ${TODAY}
|
|
5355
|
+
type: concept
|
|
5356
|
+
tags: [concept]
|
|
5357
|
+
sources: []
|
|
5358
|
+
confidence: medium
|
|
5359
|
+
provenance: research
|
|
5360
|
+
---
|
|
5361
|
+
|
|
5362
|
+
# Example Concept
|
|
5363
|
+
|
|
5364
|
+
## Overview
|
|
5365
|
+
|
|
5366
|
+
This is a seed concept page. Concept pages capture topics, patterns, and ideas that span multiple sources.
|
|
5367
|
+
|
|
5368
|
+
## Related
|
|
5369
|
+
|
|
5370
|
+
- [[example-project]]
|
|
5371
|
+
|
|
5372
|
+
## Sources
|
|
5373
|
+
|
|
5374
|
+
(Add source citations here after ingesting raw material with \`wiki-ingest\`)
|
|
5375
|
+
`
|
|
5376
|
+
};
|
|
5377
|
+
var EXAMPLE_RAW = `---
|
|
5378
|
+
source_url: https://example.com
|
|
5379
|
+
created: ${TODAY}
|
|
5380
|
+
ingested: ${TODAY}
|
|
5381
|
+
sha256: 0000000000000000000000000000000000000000000000000000000000000000
|
|
5382
|
+
---
|
|
5383
|
+
|
|
5384
|
+
# Example Source Article
|
|
5385
|
+
|
|
5386
|
+
This is a placeholder raw source. Replace it with real content ingested via \`skillwiki hash\` and the wiki-ingest skill.
|
|
5387
|
+
|
|
5388
|
+
Real sources are immutable after ingestion \u2014 never edit them.
|
|
5389
|
+
`;
|
|
5390
|
+
async function runSeed(input) {
|
|
5391
|
+
try {
|
|
5392
|
+
await stat7(join33(input.vault, "SCHEMA.md"));
|
|
5393
|
+
} catch {
|
|
5394
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
5395
|
+
}
|
|
5396
|
+
const created = [];
|
|
5397
|
+
const skipped = [];
|
|
5398
|
+
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
5399
|
+
const absPath = join33(input.vault, relPath);
|
|
5400
|
+
try {
|
|
5401
|
+
await stat7(absPath);
|
|
5402
|
+
skipped.push(relPath);
|
|
5403
|
+
} catch {
|
|
5404
|
+
await mkdir12(join33(absPath, ".."), { recursive: true });
|
|
5405
|
+
await writeFile17(absPath, content, "utf8");
|
|
5406
|
+
created.push(relPath);
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
const rawPath = join33(input.vault, "raw", "articles", "example-source.md");
|
|
5410
|
+
try {
|
|
5411
|
+
await stat7(rawPath);
|
|
5412
|
+
skipped.push("raw/articles/example-source.md");
|
|
5413
|
+
} catch {
|
|
5414
|
+
await mkdir12(join33(rawPath, ".."), { recursive: true });
|
|
5415
|
+
await writeFile17(rawPath, EXAMPLE_RAW, "utf8");
|
|
5416
|
+
created.push("raw/articles/example-source.md");
|
|
5417
|
+
}
|
|
5418
|
+
if (created.length > 0) {
|
|
5419
|
+
appendLastOp(input.vault, {
|
|
5420
|
+
operation: "seed",
|
|
5421
|
+
summary: `seeded ${created.length} example pages`,
|
|
5422
|
+
files: created,
|
|
5423
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
const hintLines = [`seeded: ${created.length}`, `skipped (already exist): ${skipped.length}`];
|
|
5427
|
+
if (created.length > 0) {
|
|
5428
|
+
hintLines.push("next steps: ingest real sources with wiki-ingest, then cite them in concept/entity pages");
|
|
5429
|
+
}
|
|
5430
|
+
return {
|
|
5431
|
+
exitCode: ExitCode.OK,
|
|
5432
|
+
result: ok({ created, skipped, humanHint: hintLines.join("\n") })
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
// src/commands/canvas.ts
|
|
5437
|
+
import { readFile as readFile21, writeFile as writeFile18 } from "fs/promises";
|
|
5438
|
+
import { existsSync as existsSync11 } from "fs";
|
|
5439
|
+
import { join as join34 } from "path";
|
|
5440
|
+
var NODE_WIDTH = 240;
|
|
5441
|
+
var NODE_HEIGHT = 60;
|
|
5442
|
+
var COLUMN_SPACING = 400;
|
|
5443
|
+
var ROW_SPACING = 80;
|
|
5444
|
+
var TYPE_COLUMNS = {
|
|
5445
|
+
entities: 0,
|
|
5446
|
+
concepts: 1,
|
|
5447
|
+
comparisons: 2,
|
|
5448
|
+
queries: 3,
|
|
5449
|
+
meta: 3
|
|
5450
|
+
};
|
|
5451
|
+
var TYPE_COLORS = {
|
|
5452
|
+
entities: "1",
|
|
5453
|
+
// red
|
|
5454
|
+
concepts: "4",
|
|
5455
|
+
// green
|
|
5456
|
+
comparisons: "2",
|
|
5457
|
+
// orange
|
|
5458
|
+
queries: "5",
|
|
5459
|
+
// cyan
|
|
5460
|
+
meta: "6"
|
|
5461
|
+
// purple
|
|
5462
|
+
};
|
|
5463
|
+
var DEFAULT_COLOR = "3";
|
|
5464
|
+
var DEFAULT_COLUMN = 2;
|
|
5465
|
+
function inferNodeType(relPath) {
|
|
5466
|
+
const segment = relPath.split("/")[0] ?? "";
|
|
5467
|
+
return TYPE_COLUMNS[segment] !== void 0 ? segment : "";
|
|
5468
|
+
}
|
|
5469
|
+
function getColumnForType(nodeType) {
|
|
5470
|
+
return TYPE_COLUMNS[nodeType] ?? DEFAULT_COLUMN;
|
|
5471
|
+
}
|
|
5472
|
+
function getColorForType(nodeType) {
|
|
5473
|
+
return TYPE_COLORS[nodeType] ?? DEFAULT_COLOR;
|
|
5474
|
+
}
|
|
5475
|
+
function buildCanvasNodes(paths) {
|
|
5476
|
+
const columnY = {};
|
|
5477
|
+
const nodes = [];
|
|
5478
|
+
for (const relPath of paths) {
|
|
5479
|
+
const nodeType = inferNodeType(relPath);
|
|
5480
|
+
const col = getColumnForType(nodeType);
|
|
5481
|
+
const y = columnY[col] ?? 0;
|
|
5482
|
+
columnY[col] = y + ROW_SPACING;
|
|
5483
|
+
nodes.push({
|
|
5484
|
+
id: relPath,
|
|
5485
|
+
type: "file",
|
|
5486
|
+
file: relPath,
|
|
5487
|
+
x: col * COLUMN_SPACING,
|
|
5488
|
+
y,
|
|
5489
|
+
width: NODE_WIDTH,
|
|
5490
|
+
height: NODE_HEIGHT,
|
|
5491
|
+
color: getColorForType(nodeType)
|
|
5492
|
+
});
|
|
5493
|
+
}
|
|
5494
|
+
return nodes;
|
|
5495
|
+
}
|
|
5496
|
+
function buildCanvasEdges(adjacency) {
|
|
5497
|
+
const edges = [];
|
|
5498
|
+
let edgeIndex = 0;
|
|
5499
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5500
|
+
for (const [source, targets] of Object.entries(adjacency)) {
|
|
5501
|
+
for (const target of targets) {
|
|
5502
|
+
const key = `${source}->${target}`;
|
|
5503
|
+
if (seen.has(key)) continue;
|
|
5504
|
+
seen.add(key);
|
|
5505
|
+
edges.push({
|
|
5506
|
+
id: `edge-${edgeIndex++}`,
|
|
5507
|
+
fromNode: source,
|
|
5508
|
+
toNode: target,
|
|
5509
|
+
fromSide: "right",
|
|
5510
|
+
toSide: "left"
|
|
5511
|
+
});
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
return edges;
|
|
5515
|
+
}
|
|
5516
|
+
async function runCanvasGenerate(input) {
|
|
5517
|
+
const graphPath = input.graphPath ?? join34(input.vault, ".skillwiki", "graph.json");
|
|
5518
|
+
if (!existsSync11(graphPath)) {
|
|
5519
|
+
return {
|
|
5520
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5521
|
+
result: err("FILE_NOT_FOUND", {
|
|
5522
|
+
path: graphPath,
|
|
5523
|
+
hint: "Run `skillwiki graph build` first to generate graph.json"
|
|
5524
|
+
})
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
let raw;
|
|
5528
|
+
try {
|
|
5529
|
+
raw = await readFile21(graphPath, "utf8");
|
|
5530
|
+
} catch (e) {
|
|
5531
|
+
return {
|
|
5532
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5533
|
+
result: err("FILE_NOT_FOUND", { path: graphPath, message: String(e) })
|
|
5534
|
+
};
|
|
5535
|
+
}
|
|
5536
|
+
let graph;
|
|
5537
|
+
try {
|
|
5538
|
+
graph = JSON.parse(raw);
|
|
5539
|
+
} catch {
|
|
5540
|
+
return {
|
|
5541
|
+
exitCode: ExitCode.SCHEMA_NOT_DETECTED,
|
|
5542
|
+
result: err("SCHEMA_NOT_DETECTED", { path: graphPath, reason: "Invalid JSON in graph.json" })
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
if (!graph.adjacency || typeof graph.adjacency !== "object") {
|
|
5546
|
+
return {
|
|
5547
|
+
exitCode: ExitCode.SCHEMA_NOT_DETECTED,
|
|
5548
|
+
result: err("SCHEMA_NOT_DETECTED", { path: graphPath, reason: "graph.json missing adjacency field" })
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5551
|
+
const paths = Object.keys(graph.adjacency);
|
|
5552
|
+
const nodes = buildCanvasNodes(paths);
|
|
5553
|
+
const edges = buildCanvasEdges(graph.adjacency);
|
|
5554
|
+
const canvas = { nodes, edges };
|
|
5555
|
+
const outPath = join34(input.vault, "vault-graph.canvas");
|
|
5556
|
+
try {
|
|
5557
|
+
await writeFile18(outPath, JSON.stringify(canvas, null, 2));
|
|
5558
|
+
} catch (e) {
|
|
5559
|
+
return {
|
|
5560
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
5561
|
+
result: err("WRITE_FAILED", { message: String(e), path: outPath })
|
|
5562
|
+
};
|
|
5563
|
+
}
|
|
5564
|
+
return {
|
|
5565
|
+
exitCode: ExitCode.OK,
|
|
5566
|
+
result: ok({
|
|
5567
|
+
out_path: outPath,
|
|
5568
|
+
node_count: nodes.length,
|
|
5569
|
+
edge_count: edges.length,
|
|
5570
|
+
humanHint: `nodes: ${nodes.length}, edges: ${edges.length}
|
|
5571
|
+
written: ${outPath}`
|
|
5572
|
+
})
|
|
5573
|
+
};
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
// src/commands/query.ts
|
|
5577
|
+
import { readFile as readFile22, stat as stat8 } from "fs/promises";
|
|
5578
|
+
import { join as join35 } from "path";
|
|
5579
|
+
var W_KEYWORD = 2;
|
|
5580
|
+
var W_SOURCE_OVERLAP = 4;
|
|
5581
|
+
var W_WIKILINK = 3;
|
|
5582
|
+
var W_ADAMIC_ADAR = 1.5;
|
|
5583
|
+
var W_TYPE_AFFINITY = 1;
|
|
5584
|
+
var NON_SEED_FACTOR = 0.4;
|
|
5585
|
+
var CONCEPT_INDICATORS = /* @__PURE__ */ new Set([
|
|
5586
|
+
"what",
|
|
5587
|
+
"how",
|
|
5588
|
+
"why",
|
|
5589
|
+
"concept",
|
|
5590
|
+
"idea",
|
|
5591
|
+
"pattern",
|
|
5592
|
+
"principle",
|
|
5593
|
+
"theory",
|
|
5594
|
+
"approach",
|
|
5595
|
+
"method",
|
|
5596
|
+
"framework",
|
|
5597
|
+
"model",
|
|
5598
|
+
"definition"
|
|
5599
|
+
]);
|
|
5600
|
+
async function runQuery(input) {
|
|
5601
|
+
const scan = await scanVault(input.vault);
|
|
5602
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
5603
|
+
const limit = input.limit ?? 10;
|
|
5604
|
+
const queryTerms = tokenize(input.text);
|
|
5605
|
+
if (queryTerms.length === 0) {
|
|
5606
|
+
return {
|
|
5607
|
+
exitCode: ExitCode.OK,
|
|
5608
|
+
result: ok({ results: [], humanHint: "no query terms" })
|
|
5609
|
+
};
|
|
5610
|
+
}
|
|
5611
|
+
const graph = await loadOrBuildGraph(input.vault);
|
|
5612
|
+
const pages = [];
|
|
5613
|
+
for (const p of scan.data.typedKnowledge) {
|
|
5614
|
+
const text = await readPage(p);
|
|
5615
|
+
const fm = extractFrontmatter(text);
|
|
5616
|
+
if (!fm.ok) continue;
|
|
5617
|
+
const title = String(fm.data.title ?? "");
|
|
5618
|
+
const type = String(fm.data.type ?? "");
|
|
5619
|
+
const tags = Array.isArray(fm.data.tags) ? fm.data.tags.map(String) : [];
|
|
5620
|
+
const sources = Array.isArray(fm.data.sources) ? fm.data.sources.map(String) : [];
|
|
5621
|
+
const split = splitFrontmatter(text);
|
|
5622
|
+
const body = split.ok ? split.data.body : text;
|
|
5623
|
+
const keywordScore = computeKeywordScore(queryTerms, title, tags, body);
|
|
5624
|
+
pages.push({ relPath: p.relPath, title, type, tags, sources, keywordScore });
|
|
5625
|
+
}
|
|
5626
|
+
const seedPaths = new Set(
|
|
5627
|
+
pages.filter((p) => p.keywordScore > 0).map((p) => p.relPath)
|
|
5628
|
+
);
|
|
5629
|
+
const results = pages.map((page) => {
|
|
5630
|
+
const sourceOverlap = scoreSourceOverlap(page, pages, seedPaths);
|
|
5631
|
+
const wikilink2 = scoreWikilink(page.relPath, seedPaths, graph);
|
|
5632
|
+
const aa = scoreAdamicAdar(page.relPath, seedPaths, graph);
|
|
5633
|
+
const typeAffinity = scoreTypeAffinity(page.type, queryTerms);
|
|
5634
|
+
const isSeed = page.keywordScore > 0;
|
|
5635
|
+
const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink2 * W_WIKILINK + aa * W_ADAMIC_ADAR;
|
|
5636
|
+
const composite = isSeed ? page.keywordScore * W_KEYWORD + structuralBoost + typeAffinity * W_TYPE_AFFINITY : structuralBoost * NON_SEED_FACTOR + typeAffinity * W_TYPE_AFFINITY;
|
|
5637
|
+
return {
|
|
5638
|
+
path: page.relPath,
|
|
5639
|
+
score: Math.round(composite * 1e3) / 1e3,
|
|
5640
|
+
title: page.title,
|
|
5641
|
+
type: page.type
|
|
5642
|
+
};
|
|
5643
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
|
|
5644
|
+
const humanHint = results.length === 0 ? "no matching pages found" : results.map((r) => `${r.path} (score: ${r.score})`).join("\n");
|
|
5645
|
+
return { exitCode: ExitCode.OK, result: ok({ results, humanHint }) };
|
|
5646
|
+
}
|
|
5647
|
+
function scoreSourceOverlap(page, allPages, seedPaths) {
|
|
5648
|
+
if (page.sources.length === 0) return 0;
|
|
5649
|
+
let total = 0;
|
|
5650
|
+
for (const seed of allPages) {
|
|
5651
|
+
if (seed.relPath === page.relPath || !seedPaths.has(seed.relPath)) continue;
|
|
5652
|
+
const shared = page.sources.filter((s) => seed.sources.includes(s)).length;
|
|
5653
|
+
total += shared;
|
|
5654
|
+
}
|
|
5655
|
+
return total;
|
|
5656
|
+
}
|
|
5657
|
+
function scoreWikilink(candidatePath, seedPaths, graph) {
|
|
5658
|
+
if (!graph) return 0;
|
|
5659
|
+
let count = 0;
|
|
5660
|
+
for (const seedPath of seedPaths) {
|
|
5661
|
+
const neighbors = graph.adjacency[seedPath];
|
|
5662
|
+
if (neighbors && neighbors.includes(candidatePath)) count++;
|
|
5663
|
+
}
|
|
5664
|
+
return count;
|
|
5665
|
+
}
|
|
5666
|
+
function scoreAdamicAdar(candidatePath, seedPaths, graph) {
|
|
5667
|
+
if (!graph) return 0;
|
|
5668
|
+
let maxScore = 0;
|
|
5669
|
+
const aaForCandidate = graph.adamicAdar[candidatePath];
|
|
5670
|
+
if (!aaForCandidate) return 0;
|
|
5671
|
+
for (const seedPath of seedPaths) {
|
|
5672
|
+
const val = aaForCandidate[seedPath];
|
|
5673
|
+
if (val !== void 0 && val > maxScore) maxScore = val;
|
|
5674
|
+
}
|
|
5675
|
+
return maxScore;
|
|
5676
|
+
}
|
|
5677
|
+
function scoreTypeAffinity(pageType, queryTerms) {
|
|
5678
|
+
const hasConceptIntent = queryTerms.some((t) => CONCEPT_INDICATORS.has(t));
|
|
5679
|
+
if (hasConceptIntent && pageType === "concept") return 1;
|
|
5680
|
+
if (!hasConceptIntent && pageType === "entity") return 0.5;
|
|
5681
|
+
return 0;
|
|
5682
|
+
}
|
|
5683
|
+
function tokenize(text) {
|
|
5684
|
+
return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
5685
|
+
}
|
|
5686
|
+
function computeKeywordScore(terms, title, tags, body) {
|
|
5687
|
+
const lowerTitle = title.toLowerCase();
|
|
5688
|
+
const lowerTags = tags.map((t) => t.toLowerCase());
|
|
5689
|
+
const lowerBody = body.toLowerCase();
|
|
5690
|
+
let score = 0;
|
|
5691
|
+
for (const term of terms) {
|
|
5692
|
+
if (lowerTitle.includes(term)) score += 3;
|
|
5693
|
+
if (lowerTags.some((t) => t.includes(term))) score += 2;
|
|
5694
|
+
if (lowerBody.includes(term)) score += 1;
|
|
5695
|
+
}
|
|
5696
|
+
return score;
|
|
5697
|
+
}
|
|
5698
|
+
async function loadOrBuildGraph(vault) {
|
|
5699
|
+
const graphPath = join35(vault, ".skillwiki", "graph.json");
|
|
5700
|
+
let needsBuild = false;
|
|
5701
|
+
try {
|
|
5702
|
+
const fileStat = await stat8(graphPath);
|
|
5703
|
+
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
5704
|
+
if (ageHours > 24) needsBuild = true;
|
|
5705
|
+
} catch {
|
|
5706
|
+
needsBuild = true;
|
|
5707
|
+
}
|
|
5708
|
+
if (needsBuild) {
|
|
5709
|
+
const buildResult = await runGraphBuild({ vault, out: graphPath });
|
|
5710
|
+
if (buildResult.exitCode !== 0) return null;
|
|
5711
|
+
}
|
|
5712
|
+
try {
|
|
5713
|
+
const raw = await readFile22(graphPath, "utf8");
|
|
5714
|
+
return JSON.parse(raw);
|
|
5715
|
+
} catch {
|
|
5716
|
+
return null;
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
// src/utils/auto-commit.ts
|
|
5721
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5722
|
+
import { join as join36 } from "path";
|
|
5723
|
+
async function postCommit(vault, exitCode) {
|
|
5724
|
+
if (exitCode !== 0) return;
|
|
5725
|
+
const home = process.env.HOME ?? "";
|
|
5726
|
+
const dotenv = await parseDotenvFile(configPath(home));
|
|
5727
|
+
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
5728
|
+
if (!existsSync12(join36(vault, ".git"))) return;
|
|
5729
|
+
const lastOps = readLastOp(vault);
|
|
5730
|
+
if (lastOps.length === 0) return;
|
|
5731
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
5732
|
+
if (!porcelain || porcelain.trim().length === 0) return;
|
|
5733
|
+
const { gitStrict: gitStrict2 } = await import("./git-M4WGJ5G3.js");
|
|
5734
|
+
try {
|
|
5735
|
+
gitStrict2(vault, ["add", "-A"]);
|
|
5736
|
+
try {
|
|
5737
|
+
gitStrict2(vault, ["reset", "HEAD", "--", ".skillwiki/last-op.json"]);
|
|
5738
|
+
} catch (_e) {
|
|
5739
|
+
}
|
|
5740
|
+
} catch (e) {
|
|
5741
|
+
process.stderr.write(`auto-commit: git add failed: ${String(e)}
|
|
5742
|
+
`);
|
|
5743
|
+
return;
|
|
5744
|
+
}
|
|
5745
|
+
const commitMessage = lastOps.map((op) => `${op.operation}: ${op.summary} (${op.files.length} files)`).join("; ");
|
|
5746
|
+
try {
|
|
5747
|
+
gitStrict2(vault, ["commit", "-m", commitMessage]);
|
|
5748
|
+
} catch (e) {
|
|
5749
|
+
process.stderr.write(`auto-commit: git commit failed: ${String(e)}
|
|
5750
|
+
`);
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5753
|
+
clearLastOp(vault);
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
// src/cli.ts
|
|
5757
|
+
var pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
|
|
5758
|
+
var program = new Command();
|
|
5759
|
+
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
5760
|
+
program.option("--human", "render terminal-readable output instead of JSON");
|
|
5761
|
+
async function emit(r, vault) {
|
|
5762
|
+
if (program.opts().human) printHuman(r.result);
|
|
5763
|
+
else printJson(r.result);
|
|
5764
|
+
if (vault) await postCommit(vault, r.exitCode);
|
|
5765
|
+
process.exit(r.exitCode);
|
|
5766
|
+
}
|
|
5767
|
+
program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
|
|
5768
|
+
program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
|
|
5769
|
+
program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
|
|
5770
|
+
let vault;
|
|
5771
|
+
if (opts.apply) {
|
|
5772
|
+
const v = await resolveVaultArg(opts.vault, opts.wiki);
|
|
5773
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5774
|
+
else vault = v.vault;
|
|
5775
|
+
}
|
|
5776
|
+
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5777
|
+
});
|
|
5778
|
+
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5779
|
+
const out = opts.out ?? join37(vault, ".skillwiki", "graph.json");
|
|
5780
|
+
emit(await runGraphBuild({ vault, out }), vault);
|
|
5781
|
+
});
|
|
5782
|
+
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
5783
|
+
canvasCmd.command("generate [vault]").description("generate .canvas from graph.json").option("--graph-path <path>", "explicit path to graph.json").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5784
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5785
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5786
|
+
else emit(await runCanvasGenerate({ vault: v.vault, graphPath: opts.graphPath }), v.vault);
|
|
5787
|
+
});
|
|
5788
|
+
program.command("overlap [vault]").description("detect typed-knowledge pages that share the same raw sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5789
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5790
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5791
|
+
else emit(await runOverlap({ vault: v.vault }), v.vault);
|
|
5792
|
+
});
|
|
5793
|
+
program.command("query <text> [vault]").description("score and rank vault pages by relevance to a query").option("--limit <n>", "max results to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (text, vault, opts) => {
|
|
5794
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5795
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5796
|
+
else emit(await runQuery({ text, vault: v.vault, limit: opts.limit }), v.vault);
|
|
5797
|
+
});
|
|
5798
|
+
program.command("orphans [vault]").description("find pages not referenced by any other page").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
|
|
5799
|
+
vault,
|
|
5800
|
+
envValue: process.env.WIKI_PATH,
|
|
5801
|
+
home: process.env.HOME ?? "",
|
|
5802
|
+
wiki: opts.wiki
|
|
5803
|
+
})));
|
|
5804
|
+
program.command("audit <file>").description("audit citation markers and source provenance for a vault page").action(async (file) => emit(await runAudit({ file })));
|
|
5805
|
+
program.command("install").description("install skillwiki SKILL.md files into ~/.claude/skills/").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").option("--symlink", "create symlinks instead of copies (dev mode \u2014 edits to source are immediately visible)", false).action(async (opts) => {
|
|
5806
|
+
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
5807
|
+
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun, symlink: !!opts.symlink }));
|
|
5808
|
+
});
|
|
5809
|
+
program.command("path").description("show the resolved vault path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--wiki <name>", "wiki profile name").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
5810
|
+
const initTime = !!opts.initTime;
|
|
5811
|
+
const flag = initTime ? opts.target : opts.vault;
|
|
5812
|
+
emit(await runPath({
|
|
5813
|
+
flag,
|
|
5814
|
+
envValue: process.env.WIKI_PATH,
|
|
5815
|
+
home: process.env.HOME ?? "",
|
|
5816
|
+
initTime,
|
|
5817
|
+
wiki: opts.wiki,
|
|
5818
|
+
explain: !!opts.explain
|
|
5819
|
+
}));
|
|
5820
|
+
});
|
|
5821
|
+
program.command("lang").description("get or set the vault language").option("--lang <code>", "explicit language override").option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
5822
|
+
emit(await runLang({
|
|
5823
|
+
flag: opts.lang,
|
|
5824
|
+
envValue: process.env.WIKI_LANG,
|
|
5825
|
+
home: process.env.HOME ?? "",
|
|
5826
|
+
explain: !!opts.explain
|
|
5827
|
+
}));
|
|
2683
5828
|
});
|
|
2684
|
-
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").option("--profile <name>", "write as named wiki profile instead of WIKI_PATH").action(async (opts) => {
|
|
5829
|
+
program.command("init").description("bootstrap a new vault with SCHEMA.md, index.md, log.md").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").option("--profile <name>", "write as named wiki profile instead of WIKI_PATH").action(async (opts) => {
|
|
2685
5830
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
2686
5831
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
2687
5832
|
emit(await runInit({
|
|
@@ -2712,37 +5857,47 @@ async function resolveVaultArg(arg, wiki) {
|
|
|
2712
5857
|
}
|
|
2713
5858
|
return { ok: true, vault: r.data.path };
|
|
2714
5859
|
}
|
|
2715
|
-
program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5860
|
+
program.command("links [vault]").description("check wikilink integrity across the vault").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5861
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5862
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5863
|
+
else emit(await runLinks({ vault: v.vault }), v.vault);
|
|
5864
|
+
});
|
|
5865
|
+
program.command("tag-audit [vault]").description("audit tag taxonomy consistency").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2716
5866
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2717
5867
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2718
|
-
else emit(await
|
|
5868
|
+
else emit(await runTagAudit({ vault: v.vault }), v.vault);
|
|
2719
5869
|
});
|
|
2720
|
-
program.command("
|
|
5870
|
+
program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2721
5871
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2722
5872
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2723
|
-
else emit(await
|
|
5873
|
+
else emit(await runIndexCheck({ vault: v.vault }), v.vault);
|
|
2724
5874
|
});
|
|
2725
|
-
program.command("index-
|
|
5875
|
+
program.command("index-link-format [vault]").description("check index.md for markdown links that should be wikilinks").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2726
5876
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2727
5877
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2728
|
-
else emit(await
|
|
5878
|
+
else emit(await runIndexLinkFormat({ vault: v.vault }), v.vault);
|
|
2729
5879
|
});
|
|
2730
|
-
program.command("
|
|
5880
|
+
program.command("topic-map-check [vault]").description("check whether a topic map page is recommended based on page count").option("--threshold <n>", "page count threshold", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2731
5881
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2732
5882
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2733
|
-
else emit(await
|
|
5883
|
+
else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
|
|
2734
5884
|
});
|
|
2735
|
-
program.command("
|
|
5885
|
+
program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2736
5886
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2737
5887
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2738
|
-
else emit(await
|
|
5888
|
+
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive }), v.vault);
|
|
2739
5889
|
});
|
|
2740
|
-
program.command("
|
|
5890
|
+
program.command("pagesize [vault]").description("report page sizes and flag oversized pages").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2741
5891
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2742
5892
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2743
|
-
else emit(await
|
|
5893
|
+
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }), v.vault);
|
|
2744
5894
|
});
|
|
2745
|
-
program.command("
|
|
5895
|
+
program.command("log-rotate [vault]").description("rotate or trim the vault log file").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5896
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5897
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5898
|
+
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
|
|
5899
|
+
});
|
|
5900
|
+
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2746
5901
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2747
5902
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2748
5903
|
else emit(await runLint({
|
|
@@ -2750,8 +5905,9 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
2750
5905
|
source: vault ? "flag" : void 0,
|
|
2751
5906
|
days: opts.days,
|
|
2752
5907
|
lines: opts.lines,
|
|
2753
|
-
logThreshold: opts.logThreshold
|
|
2754
|
-
|
|
5908
|
+
logThreshold: opts.logThreshold,
|
|
5909
|
+
fix: opts.fix ?? false
|
|
5910
|
+
}), v.vault);
|
|
2755
5911
|
});
|
|
2756
5912
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
2757
5913
|
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
@@ -2765,42 +5921,159 @@ program.command("doctor").description("diagnose skillwiki setup issues").action(
|
|
|
2765
5921
|
currentVersion: pkg.version,
|
|
2766
5922
|
cwd: process.cwd()
|
|
2767
5923
|
})));
|
|
2768
|
-
program.command("
|
|
5924
|
+
program.command("status [vault]").description("output vault diagnostics").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2769
5925
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2770
5926
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2771
|
-
else emit(await
|
|
5927
|
+
else emit(await runStatus({
|
|
5928
|
+
vault: v.vault,
|
|
5929
|
+
home: process.env.HOME ?? "",
|
|
5930
|
+
langEnvValue: process.env.WIKI_LANG
|
|
5931
|
+
}), v.vault);
|
|
5932
|
+
});
|
|
5933
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
5934
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5935
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5936
|
+
else emit(await runArchive({ vault: v.vault, page }), v.vault);
|
|
2772
5937
|
});
|
|
2773
5938
|
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2774
5939
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2775
5940
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2776
|
-
else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }));
|
|
5941
|
+
else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }), v.vault);
|
|
2777
5942
|
});
|
|
2778
5943
|
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2779
5944
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2780
5945
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2781
|
-
else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
|
|
5946
|
+
else emit(await runDedup({ vault: v.vault, apply: opts.apply }), v.vault);
|
|
2782
5947
|
});
|
|
2783
5948
|
program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2784
5949
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2785
5950
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2786
|
-
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
5951
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
2787
5952
|
});
|
|
2788
5953
|
program.command("frontmatter-fix [vault]").description("fix common frontmatter issues on typed-knowledge pages").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2789
5954
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2790
5955
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2791
|
-
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
5956
|
+
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
2792
5957
|
});
|
|
2793
5958
|
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
2794
5959
|
home: process.env.HOME ?? "",
|
|
2795
5960
|
distTag: opts.tag
|
|
2796
5961
|
})));
|
|
5962
|
+
program.command("self-update").description("update skillwiki CLI from local source or npm@beta").option("--check", "check for updates without installing", false).action(async (opts) => emit(await runSelfUpdate({
|
|
5963
|
+
home: process.env.HOME ?? "",
|
|
5964
|
+
check: !!opts.check
|
|
5965
|
+
})));
|
|
2797
5966
|
program.command("transcripts [vault]").description("list transcript files in raw/transcripts/").option("--since <date>", "only files ingested on or after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2798
5967
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2799
5968
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2800
|
-
else emit(await runTranscripts({ vault: v.vault, since: opts.since }));
|
|
5969
|
+
else emit(await runTranscripts({ vault: v.vault, since: opts.since }), v.vault);
|
|
5970
|
+
});
|
|
5971
|
+
program.command("project-index <slug> [vault]").description("generate a knowledge index for a project workspace").option("--apply", "write knowledge.md to the project directory", false).option("--wiki <name>", "wiki profile name").action(async (slug, vault, opts) => {
|
|
5972
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5973
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5974
|
+
else emit(await runProjectIndex({ vault: v.vault, slug, apply: !!opts.apply }), v.vault);
|
|
5975
|
+
});
|
|
5976
|
+
var compoundCmd = program.command("compound").description("manage project compound entries");
|
|
5977
|
+
compoundCmd.command("promote [vault]").description("promote retros with Generalize?: yes to compound entries").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--dry-run", "preview promotions without writing files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5978
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5979
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5980
|
+
else emit(await runCompound({ vault: v.vault, project: opts.project, dryRun: !!opts.dryRun }), v.vault);
|
|
5981
|
+
});
|
|
5982
|
+
compoundCmd.command("list [vault]").description("list compound entries for a project").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5983
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5984
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5985
|
+
else emit(await runCompoundList({ vault: v.vault, project: opts.project }));
|
|
2801
5986
|
});
|
|
5987
|
+
compoundCmd.command("delete <entry> [vault]").description("delete a compound entry and regenerate knowledge index").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--wiki <name>", "wiki profile name").action(async (entry, vault, opts) => {
|
|
5988
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5989
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5990
|
+
else emit(await runCompoundDelete({ vault: v.vault, project: opts.project, entry }), v.vault);
|
|
5991
|
+
});
|
|
5992
|
+
program.command("tag-sync [vault]").description("mirror frontmatter enum values to nested Obsidian tags").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5993
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5994
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5995
|
+
else emit(await runTagSync({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
5996
|
+
});
|
|
5997
|
+
var syncCmd = program.command("sync").description("manage vault sync");
|
|
5998
|
+
syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5999
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6000
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6001
|
+
else emit(runSyncStatus({ vault: v.vault }));
|
|
6002
|
+
});
|
|
6003
|
+
syncCmd.command("push [vault]").description("lint, commit, and push vault changes to remote").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6004
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6005
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6006
|
+
else emit(await runSyncPush({ vault: v.vault }));
|
|
6007
|
+
});
|
|
6008
|
+
syncCmd.command("pull [vault]").description("pull remote vault changes and lint").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6009
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6010
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6011
|
+
else emit(await runSyncPull({ vault: v.vault }), v.vault);
|
|
6012
|
+
});
|
|
6013
|
+
var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
|
|
6014
|
+
backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6015
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6016
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6017
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6018
|
+
const dotenv = await parseDotenvFile(configPath(home));
|
|
6019
|
+
emit(await runBackupSync({
|
|
6020
|
+
vault: v.vault,
|
|
6021
|
+
bucket: opts.bucket ?? dotenv["BACKUP_BUCKET"] ?? "",
|
|
6022
|
+
endpoint: opts.endpoint ?? dotenv["BACKUP_ENDPOINT"] ?? "",
|
|
6023
|
+
region: opts.region ?? dotenv["BACKUP_REGION"] ?? "us-east-1",
|
|
6024
|
+
accessKeyId: dotenv["BACKUP_ACCESS_KEY_ID"] ?? "",
|
|
6025
|
+
secretAccessKey: dotenv["BACKUP_SECRET_ACCESS_KEY"] ?? "",
|
|
6026
|
+
dryRun: opts.dryRun,
|
|
6027
|
+
prune: opts.prune
|
|
6028
|
+
}), v.vault);
|
|
6029
|
+
});
|
|
6030
|
+
backupCmd.command("restore [vault]").description("restore vault from S3-compatible remote backup").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--target <dir>", "restore target directory (defaults to vault)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6031
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6032
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6033
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6034
|
+
const dotenv = await parseDotenvFile(configPath(home));
|
|
6035
|
+
emit(await runBackupRestore({
|
|
6036
|
+
vault: v.vault,
|
|
6037
|
+
bucket: opts.bucket ?? dotenv["BACKUP_BUCKET"] ?? "",
|
|
6038
|
+
endpoint: opts.endpoint ?? dotenv["BACKUP_ENDPOINT"] ?? "",
|
|
6039
|
+
region: opts.region ?? dotenv["BACKUP_REGION"] ?? "us-east-1",
|
|
6040
|
+
accessKeyId: dotenv["BACKUP_ACCESS_KEY_ID"] ?? "",
|
|
6041
|
+
secretAccessKey: dotenv["BACKUP_SECRET_ACCESS_KEY"] ?? "",
|
|
6042
|
+
target: opts.target
|
|
6043
|
+
}), v.vault);
|
|
6044
|
+
});
|
|
6045
|
+
program.command("seed [vault]").description("populate a vault with example content").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6046
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6047
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6048
|
+
else emit(await runSeed({ vault: v.vault }), v.vault);
|
|
6049
|
+
});
|
|
6050
|
+
program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "note").option("--project <slug>", "associated project slug").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6051
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6052
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6053
|
+
else emit(await runObserve({
|
|
6054
|
+
vault: v.vault,
|
|
6055
|
+
text: opts.text,
|
|
6056
|
+
kind: opts.kind,
|
|
6057
|
+
project: opts.project
|
|
6058
|
+
}), v.vault);
|
|
6059
|
+
});
|
|
6060
|
+
program.command("ingest <source>").description("ingest a source URL or local file into the vault").requiredOption("--vault <path>", "vault root directory").requiredOption("--type <type>", "typed-knowledge type (entity|concept|comparison|query)").requiredOption("--title <title>", "page title").option("--tags <csv>", "comma-separated tags").option("--provenance <provenance>", "provenance (research|project)").option("--dry-run", "preview without writing files", false).action(async (source, opts) => {
|
|
6061
|
+
const tags = typeof opts.tags === "string" ? opts.tags.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
|
|
6062
|
+
emit(await runIngest({
|
|
6063
|
+
source,
|
|
6064
|
+
vault: opts.vault,
|
|
6065
|
+
type: opts.type,
|
|
6066
|
+
title: opts.title,
|
|
6067
|
+
tags,
|
|
6068
|
+
provenance: opts.provenance,
|
|
6069
|
+
dryRun: !!opts.dryRun
|
|
6070
|
+
}), opts.vault);
|
|
6071
|
+
});
|
|
6072
|
+
for (const w of getDeprecatedWarnings(process.env.HOME ?? "")) {
|
|
6073
|
+
process.stderr.write(w + "\n");
|
|
6074
|
+
}
|
|
2802
6075
|
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
2803
6076
|
program.parseAsync(process.argv).catch((e) => {
|
|
2804
6077
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
2805
|
-
process.exit(
|
|
6078
|
+
process.exit(ExitCode.INTERNAL_ERROR);
|
|
2806
6079
|
});
|