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 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 join6, resolve } from "path";
7
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
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 writeFileSync2 } from "fs";
24
- import { join as join4, dirname as dirname3 } from "path";
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 = join4(targetDir, relativePath);
233
+ const full = join5(targetDir, relativePath);
184
234
  mkdirSync2(dirname3(full), { recursive: true });
185
- writeFileSync2(full, content, "utf8");
235
+ writeFileSync3(full, content, "utf8");
186
236
  }
187
237
  function touch(targetDir, relativePath) {
188
- const full = join4(targetDir, relativePath);
238
+ const full = join5(targetDir, relativePath);
189
239
  mkdirSync2(dirname3(full), { recursive: true });
190
- writeFileSync2(full, "", "utf8");
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", join4(targetDir, vars.CONTENT_DIR).replace(/\\/g, "/")]
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
- const profile = {
295
- ...tempProfile,
296
- _checksums: checksums
297
- };
298
- writeRaw(targetDir, "profile.yaml", yaml2.dump(profile));
299
- writeRaw(targetDir, ".gitignore", ".obsidian/\n.DS_Store\n");
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 existsSync4, readFileSync as readFileSync4, readdirSync } from "fs";
304
- import { join as join5, relative, sep, basename } from "path";
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 existsSync4(join5(cwd, "profile.yaml")) ? cwd : null;
359
+ return existsSync5(join6(cwd, "profile.yaml")) ? cwd : null;
310
360
  }
311
361
  function listMarkdownFiles(dir) {
312
- if (!existsSync4(dir)) return [];
362
+ if (!existsSync5(dir)) return [];
313
363
  const results = [];
314
364
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
315
- const fullPath = join5(dir, entry.name);
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 = join5(root, profile.content_dir ?? "graph");
369
- const notesDir = join5(contentDir, NOTES);
370
- const skillsDir = join5(contentDir, SKILLS);
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 = readFileSync4(skillFile, "utf8");
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 = join5(root, profile.content_dir ?? "graph");
392
- const notesDir = join5(contentDir, NOTES);
393
- const postsDir = join5(contentDir, POSTS);
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 = readFileSync4(file, "utf8");
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 = join5(root, profile.content_dir ?? "graph");
419
- const notesDir = join5(contentDir, NOTES);
420
- const exclusionsPath = join5(notesDir, "exclusions.md");
421
- if (!existsSync4(exclusionsPath)) return [];
422
- const content = readFileSync4(exclusionsPath, "utf8");
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 = join5(contentDir, SKILLS);
426
- const postsDir = join5(contentDir, POSTS);
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 = readFileSync4(file, "utf8");
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 = join5(root, profile.content_dir ?? "graph");
517
+ const contentDir = join6(root, profile.content_dir ?? "graph");
468
518
  const scannedFiles = /* @__PURE__ */ new Set([
469
- ...listMarkdownFiles(join5(contentDir, NOTES)),
470
- ...listMarkdownFiles(join5(contentDir, SKILLS)),
471
- ...listMarkdownFiles(join5(contentDir, POSTS))
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 = join6(cwd, "profile.yaml");
666
- writeFileSync3(full, yaml3.dump(profile), "utf8");
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 = join6(targetDir, rel);
670
- if (!existsSync5(fullPath)) return "deleted";
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._checksums ?? {};
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
- updatedProfile._checksums = newChecksums;
750
- writeProfile(cwd, updatedProfile);
751
- return { profile: updatedProfile, updated, skipped };
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._checksums ?? {};
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 = join6(cwd, relativePath);
850
- if (!existsSync5(fullPath)) {
900
+ const fullPath = join7(cwd, relativePath);
901
+ if (!existsSync6(fullPath)) {
851
902
  mkdirSync3(dirname4(fullPath), { recursive: true });
852
- writeFileSync3(fullPath, content, "utf8");
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
- updatedProfile._checksums = newChecksums;
878
- writeProfile(cwd, updatedProfile);
879
- return { profile: updatedProfile, added, skipped: skippedFiles, deleted, kept };
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 ?? [];
@@ -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 `/include` and describe something you've done. Claude asks a few questions and writes a note to `{{CONTENT_DIR}}/notes/`.
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 `/include <description>` and Claude will ask clarifying questions and write the note for you.
15
+ Run `/add <description>` and Claude will ask clarifying questions and write the note for you.
16
16
 
17
17
  ## File naming
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-patina",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Personal professional knowledge graph — setup and management",
5
5
  "type": "module",
6
6
  "bin": {