my-patina 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +107 -55
- package/dist/templates/CLAUDE.md +1 -1
- package/dist/templates/graph/notes/README.md +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/wizard.ts
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import { dirname as dirname4, join as
|
|
7
|
-
import { existsSync as
|
|
6
|
+
import { dirname as dirname4, join as join7, resolve } from "path";
|
|
7
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
8
8
|
import yaml3 from "js-yaml";
|
|
9
9
|
|
|
10
10
|
// src/detect.ts
|
|
@@ -20,8 +20,8 @@ function loadProfile(root) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// src/scaffold.ts
|
|
23
|
-
import { mkdirSync as mkdirSync2, writeFileSync as
|
|
24
|
-
import { join as
|
|
23
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
24
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
25
25
|
import yaml2 from "js-yaml";
|
|
26
26
|
|
|
27
27
|
// src/template.ts
|
|
@@ -178,16 +178,66 @@ function writeManagedFile(targetDir, relativePath, newContent, storedChecksums)
|
|
|
178
178
|
return { outcome: "skipped", checksum: storedHash };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// src/state.ts
|
|
182
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
183
|
+
import { join as join4 } from "path";
|
|
184
|
+
var STATE_FILENAME = ".patina-state.json";
|
|
185
|
+
function normalizeChecksums(checksums) {
|
|
186
|
+
const result = {};
|
|
187
|
+
for (const [key, value] of Object.entries(checksums)) {
|
|
188
|
+
result[key.replace(/\\/g, "/")] = value;
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function readState(root, profile) {
|
|
193
|
+
const statePath = join4(root, STATE_FILENAME);
|
|
194
|
+
if (existsSync4(statePath)) {
|
|
195
|
+
const raw = readFileSync4(statePath, "utf8");
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Corrupt ${STATE_FILENAME}: failed to parse JSON. Fix or delete the file at ${statePath} and try again.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const obj = parsed;
|
|
205
|
+
const rawChecksums = obj.checksums;
|
|
206
|
+
if (rawChecksums !== void 0 && (typeof rawChecksums !== "object" || Array.isArray(rawChecksums) || rawChecksums === null)) {
|
|
207
|
+
throw new Error(`Corrupt ${STATE_FILENAME}: 'checksums' must be an object at ${statePath}.`);
|
|
208
|
+
}
|
|
209
|
+
const checksums2 = normalizeChecksums(rawChecksums ?? {});
|
|
210
|
+
return { checksums: checksums2 };
|
|
211
|
+
}
|
|
212
|
+
const legacyChecksums = profile?._checksums;
|
|
213
|
+
const checksums = normalizeChecksums(legacyChecksums ?? {});
|
|
214
|
+
return { checksums };
|
|
215
|
+
}
|
|
216
|
+
function stripLegacyChecksums(profile) {
|
|
217
|
+
const { _checksums: _stripped, ...clean } = profile;
|
|
218
|
+
return clean;
|
|
219
|
+
}
|
|
220
|
+
function writeState(root, state) {
|
|
221
|
+
const normalized = {
|
|
222
|
+
checksums: normalizeChecksums(state.checksums)
|
|
223
|
+
};
|
|
224
|
+
writeFileSync2(
|
|
225
|
+
join4(root, STATE_FILENAME),
|
|
226
|
+
JSON.stringify(normalized, null, 2) + "\n",
|
|
227
|
+
"utf8"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
181
231
|
// src/scaffold.ts
|
|
182
232
|
function writeRaw(targetDir, relativePath, content) {
|
|
183
|
-
const full =
|
|
233
|
+
const full = join5(targetDir, relativePath);
|
|
184
234
|
mkdirSync2(dirname3(full), { recursive: true });
|
|
185
|
-
|
|
235
|
+
writeFileSync3(full, content, "utf8");
|
|
186
236
|
}
|
|
187
237
|
function touch(targetDir, relativePath) {
|
|
188
|
-
const full =
|
|
238
|
+
const full = join5(targetDir, relativePath);
|
|
189
239
|
mkdirSync2(dirname3(full), { recursive: true });
|
|
190
|
-
|
|
240
|
+
writeFileSync3(full, "", "utf8");
|
|
191
241
|
}
|
|
192
242
|
function profileToVars(profile, liProfileUrl) {
|
|
193
243
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -216,7 +266,7 @@ function baseManagedFiles(vars, editor, targetDir) {
|
|
|
216
266
|
mcpServers: {
|
|
217
267
|
obsidian: {
|
|
218
268
|
command: "npx",
|
|
219
|
-
args: ["-y", "mcp-obsidian@latest",
|
|
269
|
+
args: ["-y", "mcp-obsidian@latest", join5(targetDir, vars.CONTENT_DIR).replace(/\\/g, "/")]
|
|
220
270
|
}
|
|
221
271
|
}
|
|
222
272
|
};
|
|
@@ -291,28 +341,28 @@ async function scaffold(opts) {
|
|
|
291
341
|
writeRaw(targetDir, relativePath, content);
|
|
292
342
|
}
|
|
293
343
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
344
|
+
writeRaw(targetDir, "profile.yaml", yaml2.dump(tempProfile));
|
|
345
|
+
writeState(targetDir, { checksums });
|
|
346
|
+
writeRaw(targetDir, ".gitignore", `.obsidian/
|
|
347
|
+
.DS_Store
|
|
348
|
+
${STATE_FILENAME}
|
|
349
|
+
`);
|
|
300
350
|
}
|
|
301
351
|
|
|
302
352
|
// src/validate.ts
|
|
303
|
-
import { existsSync as
|
|
304
|
-
import { join as
|
|
353
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync } from "fs";
|
|
354
|
+
import { join as join6, relative, sep, basename } from "path";
|
|
305
355
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
306
356
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
307
357
|
var POSTS = CONTENT_SUBDIRS[2];
|
|
308
358
|
function findPatinaRoot(cwd) {
|
|
309
|
-
return
|
|
359
|
+
return existsSync5(join6(cwd, "profile.yaml")) ? cwd : null;
|
|
310
360
|
}
|
|
311
361
|
function listMarkdownFiles(dir) {
|
|
312
|
-
if (!
|
|
362
|
+
if (!existsSync5(dir)) return [];
|
|
313
363
|
const results = [];
|
|
314
364
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
315
|
-
const fullPath =
|
|
365
|
+
const fullPath = join6(dir, entry.name);
|
|
316
366
|
if (entry.isDirectory()) {
|
|
317
367
|
results.push(...listMarkdownFiles(fullPath));
|
|
318
368
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -365,14 +415,14 @@ function parseExclusions(markdown) {
|
|
|
365
415
|
return [...new Set(items)];
|
|
366
416
|
}
|
|
367
417
|
function checkSkillNotes(root, profile) {
|
|
368
|
-
const contentDir =
|
|
369
|
-
const notesDir =
|
|
370
|
-
const skillsDir =
|
|
418
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
419
|
+
const notesDir = join6(contentDir, NOTES);
|
|
420
|
+
const skillsDir = join6(contentDir, SKILLS);
|
|
371
421
|
const noteFiles = listMarkdownFiles(notesDir);
|
|
372
422
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
373
423
|
const issues = [];
|
|
374
424
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
375
|
-
const content =
|
|
425
|
+
const content = readFileSync5(skillFile, "utf8");
|
|
376
426
|
const links = extractWikiLinks(content);
|
|
377
427
|
for (const { target, line } of links) {
|
|
378
428
|
if (!noteSlugs.has(target)) {
|
|
@@ -388,9 +438,9 @@ function checkSkillNotes(root, profile) {
|
|
|
388
438
|
return issues;
|
|
389
439
|
}
|
|
390
440
|
function checkWikiLinks(root, profile) {
|
|
391
|
-
const contentDir =
|
|
392
|
-
const notesDir =
|
|
393
|
-
const postsDir =
|
|
441
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
442
|
+
const notesDir = join6(contentDir, NOTES);
|
|
443
|
+
const postsDir = join6(contentDir, POSTS);
|
|
394
444
|
const noteFiles = listMarkdownFiles(notesDir);
|
|
395
445
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
396
446
|
const issues = [];
|
|
@@ -399,7 +449,7 @@ function checkWikiLinks(root, profile) {
|
|
|
399
449
|
...listMarkdownFiles(postsDir)
|
|
400
450
|
];
|
|
401
451
|
for (const file of filesToScan) {
|
|
402
|
-
const content =
|
|
452
|
+
const content = readFileSync5(file, "utf8");
|
|
403
453
|
const links = extractWikiLinks(content);
|
|
404
454
|
for (const { target, line } of links) {
|
|
405
455
|
if (!noteSlugs.has(target)) {
|
|
@@ -415,15 +465,15 @@ function checkWikiLinks(root, profile) {
|
|
|
415
465
|
return issues;
|
|
416
466
|
}
|
|
417
467
|
function checkExclusions(root, profile) {
|
|
418
|
-
const contentDir =
|
|
419
|
-
const notesDir =
|
|
420
|
-
const exclusionsPath =
|
|
421
|
-
if (!
|
|
422
|
-
const content =
|
|
468
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
469
|
+
const notesDir = join6(contentDir, NOTES);
|
|
470
|
+
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
471
|
+
if (!existsSync5(exclusionsPath)) return [];
|
|
472
|
+
const content = readFileSync5(exclusionsPath, "utf8");
|
|
423
473
|
const items = parseExclusions(content);
|
|
424
474
|
if (items.length === 0) return [];
|
|
425
|
-
const skillsDir =
|
|
426
|
-
const postsDir =
|
|
475
|
+
const skillsDir = join6(contentDir, SKILLS);
|
|
476
|
+
const postsDir = join6(contentDir, POSTS);
|
|
427
477
|
const filesToScan = [
|
|
428
478
|
...listMarkdownFiles(skillsDir),
|
|
429
479
|
...listMarkdownFiles(postsDir)
|
|
@@ -431,7 +481,7 @@ function checkExclusions(root, profile) {
|
|
|
431
481
|
const issues = [];
|
|
432
482
|
const seen = /* @__PURE__ */ new Set();
|
|
433
483
|
for (const file of filesToScan) {
|
|
434
|
-
const fileContent =
|
|
484
|
+
const fileContent = readFileSync5(file, "utf8");
|
|
435
485
|
const lines = fileContent.split("\n");
|
|
436
486
|
for (let i = 0; i < lines.length; i++) {
|
|
437
487
|
const lineText = lines[i];
|
|
@@ -464,11 +514,11 @@ function validate(root, profile) {
|
|
|
464
514
|
if (a.file > b.file) return 1;
|
|
465
515
|
return (a.line ?? 0) - (b.line ?? 0);
|
|
466
516
|
});
|
|
467
|
-
const contentDir =
|
|
517
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
468
518
|
const scannedFiles = /* @__PURE__ */ new Set([
|
|
469
|
-
...listMarkdownFiles(
|
|
470
|
-
...listMarkdownFiles(
|
|
471
|
-
...listMarkdownFiles(
|
|
519
|
+
...listMarkdownFiles(join6(contentDir, NOTES)),
|
|
520
|
+
...listMarkdownFiles(join6(contentDir, SKILLS)),
|
|
521
|
+
...listMarkdownFiles(join6(contentDir, POSTS))
|
|
472
522
|
]);
|
|
473
523
|
return {
|
|
474
524
|
ok: allIssues.length === 0,
|
|
@@ -662,12 +712,12 @@ async function runInstall(cwd) {
|
|
|
662
712
|
p.outro(chalk.hex("#94A3B8")("Run claude from inside your patina to get started."));
|
|
663
713
|
}
|
|
664
714
|
function writeProfile(cwd, profile) {
|
|
665
|
-
const full =
|
|
666
|
-
|
|
715
|
+
const full = join7(cwd, "profile.yaml");
|
|
716
|
+
writeFileSync4(full, yaml3.dump(profile), "utf8");
|
|
667
717
|
}
|
|
668
718
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
669
|
-
const fullPath =
|
|
670
|
-
if (!
|
|
719
|
+
const fullPath = join7(targetDir, rel);
|
|
720
|
+
if (!existsSync6(fullPath)) return "deleted";
|
|
671
721
|
const currentHash = hashFile(fullPath);
|
|
672
722
|
const storedHash = stored[rel];
|
|
673
723
|
if (storedHash && currentHash !== storedHash) {
|
|
@@ -724,7 +774,7 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
724
774
|
}
|
|
725
775
|
};
|
|
726
776
|
const vars = profileToVars(updatedProfile);
|
|
727
|
-
const stored = profile.
|
|
777
|
+
const stored = readState(cwd, profile).checksums;
|
|
728
778
|
const newChecksums = {};
|
|
729
779
|
const files = [
|
|
730
780
|
...baseManagedFiles(vars, updatedProfile.editor, cwd),
|
|
@@ -746,9 +796,10 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
746
796
|
newChecksums[rel] = hash;
|
|
747
797
|
}
|
|
748
798
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
799
|
+
writeState(cwd, { checksums: newChecksums });
|
|
800
|
+
const profileToWrite = stripLegacyChecksums(updatedProfile);
|
|
801
|
+
writeProfile(cwd, profileToWrite);
|
|
802
|
+
return { profile: profileToWrite, updated, skipped };
|
|
752
803
|
}
|
|
753
804
|
async function runUpdateProfile(cwd, profile) {
|
|
754
805
|
console.log("");
|
|
@@ -822,7 +873,7 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
822
873
|
p.outro(chalk.hex("#94A3B8")("Profile updated."));
|
|
823
874
|
}
|
|
824
875
|
function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
825
|
-
const stored = profile.
|
|
876
|
+
const stored = readState(cwd, profile).checksums;
|
|
826
877
|
const newChecksums = { ...stored };
|
|
827
878
|
let updatedProfile = { ...profile, modules: [...profile.modules ?? []] };
|
|
828
879
|
const added = [];
|
|
@@ -846,10 +897,10 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
846
897
|
}
|
|
847
898
|
}
|
|
848
899
|
for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
|
|
849
|
-
const fullPath =
|
|
850
|
-
if (!
|
|
900
|
+
const fullPath = join7(cwd, relativePath);
|
|
901
|
+
if (!existsSync6(fullPath)) {
|
|
851
902
|
mkdirSync3(dirname4(fullPath), { recursive: true });
|
|
852
|
-
|
|
903
|
+
writeFileSync4(fullPath, content, "utf8");
|
|
853
904
|
added.push(relativePath);
|
|
854
905
|
}
|
|
855
906
|
}
|
|
@@ -874,9 +925,10 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
874
925
|
updatedProfile = def.onRemove(updatedProfile);
|
|
875
926
|
}
|
|
876
927
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
928
|
+
writeState(cwd, { checksums: newChecksums });
|
|
929
|
+
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
930
|
+
writeProfile(cwd, finalProfile);
|
|
931
|
+
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept };
|
|
880
932
|
}
|
|
881
933
|
async function runUpdateModules(cwd, profile) {
|
|
882
934
|
const currentModules = profile.modules ?? [];
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -29,7 +29,7 @@ The graph is the source of truth. Nothing gets added to generated content unless
|
|
|
29
29
|
|
|
30
30
|
## How it works
|
|
31
31
|
|
|
32
|
-
**Adding evidence:** Run `/
|
|
32
|
+
**Adding evidence:** Run `/add` and describe something you've done. Claude asks a few questions and writes a note to `{{CONTENT_DIR}}/notes/`.
|
|
33
33
|
|
|
34
34
|
**Reviewing skills:** Run `/skill-search` to audit your notes for skill gaps, project completions, and stale entries.
|
|
35
35
|
|
|
@@ -12,7 +12,7 @@ Notes here are **first-class evidence** — weighted the same as weekly summarie
|
|
|
12
12
|
|
|
13
13
|
## How to add a note
|
|
14
14
|
|
|
15
|
-
Run `/
|
|
15
|
+
Run `/add <description>` and Claude will ask clarifying questions and write the note for you.
|
|
16
16
|
|
|
17
17
|
## File naming
|
|
18
18
|
|