my-patina 0.4.0 → 0.6.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
@@ -7,7 +7,7 @@ import { Command } from "commander";
7
7
  import * as p from "@clack/prompts";
8
8
  import chalk from "chalk";
9
9
  import { dirname as dirname4, join as join7, resolve } from "path";
10
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
10
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
11
11
  import yaml3 from "js-yaml";
12
12
 
13
13
  // src/detect.ts
@@ -35,7 +35,7 @@ function render(template, vars) {
35
35
  }
36
36
 
37
37
  // src/upgrade.ts
38
- import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
38
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
39
39
  import { join as join3, dirname as dirname2 } from "path";
40
40
 
41
41
  // src/checksums.ts
@@ -163,8 +163,122 @@ var CONTENT_SUBDIRS = ["notes", "skills", "posts"];
163
163
  var MODULE_MANAGED_FILES = Object.fromEntries(MODULES.map((m) => [m.id, m.managedPaths]));
164
164
  var MODULE_CONTENT_FILES = Object.fromEntries(MODULES.map((m) => [m.id, m.contentFileNames]));
165
165
 
166
+ // src/sections.ts
167
+ function hasFences(content) {
168
+ return parseSections(content).length > 0;
169
+ }
170
+ function parseSections(content) {
171
+ const sections = [];
172
+ const startRe = /<!-- patina:([a-z0-9-]+):start -->/g;
173
+ let match;
174
+ while ((match = startRe.exec(content)) !== null) {
175
+ const id = match[1];
176
+ const startIdx = match.index;
177
+ const afterStart = match.index + match[0].length;
178
+ const endMarker = `<!-- patina:${id}:end -->`;
179
+ const endIdx = content.indexOf(endMarker, afterStart);
180
+ if (endIdx === -1) {
181
+ continue;
182
+ }
183
+ let innerStart = afterStart;
184
+ if (content[innerStart] === "\r") innerStart++;
185
+ if (content[innerStart] === "\n") innerStart++;
186
+ let innerEnd = endIdx;
187
+ if (innerEnd > 0 && content[innerEnd - 1] === "\n") innerEnd--;
188
+ if (innerEnd > 0 && content[innerEnd - 1] === "\r") innerEnd--;
189
+ const inner = content.slice(innerStart, innerEnd).replace(/\r\n/g, "\n");
190
+ sections.push({
191
+ id,
192
+ inner,
193
+ start: startIdx,
194
+ end: endIdx + endMarker.length
195
+ });
196
+ }
197
+ return sections;
198
+ }
199
+ function renderSection(id, innerContent) {
200
+ return `<!-- patina:${id}:start -->
201
+ ${innerContent}
202
+ <!-- patina:${id}:end -->`;
203
+ }
204
+ function mergeSections(existing, newSections, storedChecksums, relativePath, overwrite) {
205
+ const existingSections = parseSections(existing);
206
+ const outcomes = [];
207
+ const existingMap = new Map(
208
+ existingSections.map((s) => [s.id, s])
209
+ );
210
+ let result = existing;
211
+ const replacements = [];
212
+ for (const section of existingSections) {
213
+ const { id, inner } = section;
214
+ if (id in newSections) {
215
+ const newInner = newSections[id];
216
+ const storedKey = `${relativePath}:${id}`;
217
+ const storedHash = storedChecksums[storedKey];
218
+ const currentHash = hashContent(inner);
219
+ if (overwrite.has(id)) {
220
+ const normalized = newInner.replace(/\r\n/g, "\n");
221
+ replacements.push({
222
+ start: section.start,
223
+ end: section.end,
224
+ replacement: renderSection(id, normalized)
225
+ });
226
+ outcomes.push({ id, outcome: "updated", newChecksum: hashContent(normalized) });
227
+ } else if (storedHash && currentHash !== storedHash) {
228
+ outcomes.push({ id, outcome: "skipped", newChecksum: storedHash });
229
+ } else {
230
+ const normalized = newInner.replace(/\r\n/g, "\n");
231
+ const normalizedHash = hashContent(normalized);
232
+ if (normalizedHash === currentHash) {
233
+ outcomes.push({ id, outcome: "unchanged", newChecksum: currentHash });
234
+ } else {
235
+ replacements.push({
236
+ start: section.start,
237
+ end: section.end,
238
+ replacement: renderSection(id, normalized)
239
+ });
240
+ outcomes.push({ id, outcome: "updated", newChecksum: normalizedHash });
241
+ }
242
+ }
243
+ } else {
244
+ outcomes.push({ id, outcome: "unchanged", newChecksum: hashContent(inner) });
245
+ }
246
+ }
247
+ replacements.sort((a, b) => b.start - a.start);
248
+ for (const { start, end, replacement } of replacements) {
249
+ result = result.slice(0, start) + replacement + result.slice(end);
250
+ }
251
+ for (const [id, innerContent] of Object.entries(newSections)) {
252
+ if (!existingMap.has(id)) {
253
+ const normalized = innerContent.replace(/\r\n/g, "\n");
254
+ const block = renderSection(id, normalized);
255
+ result = result.endsWith("\n") ? result + block + "\n" : result + "\n" + block + "\n";
256
+ outcomes.push({ id, outcome: "added", newChecksum: hashContent(normalized) });
257
+ }
258
+ }
259
+ return { content: result, sections: outcomes };
260
+ }
261
+ function inspectSections(relativePath, existing, storedChecksums) {
262
+ const sections = parseSections(existing);
263
+ const editedIds = [];
264
+ for (const { id, inner } of sections) {
265
+ const storedKey = `${relativePath}:${id}`;
266
+ const storedHash = storedChecksums[storedKey];
267
+ if (!storedHash) {
268
+ continue;
269
+ }
270
+ if (hashContent(inner) !== storedHash) {
271
+ editedIds.push(id);
272
+ }
273
+ }
274
+ return editedIds;
275
+ }
276
+
166
277
  // src/upgrade.ts
167
- function writeManagedFile(targetDir, relativePath, newContent, storedChecksums) {
278
+ function writeManagedFile(targetDir, relativePath, newContent, storedChecksums, overwrite) {
279
+ if (hasFences(newContent)) {
280
+ return writeSectionedFile(targetDir, relativePath, newContent, storedChecksums, overwrite ?? /* @__PURE__ */ new Set());
281
+ }
168
282
  const fullPath = join3(targetDir, relativePath);
169
283
  const newChecksum = hashContent(newContent);
170
284
  if (!existsSync3(fullPath)) {
@@ -180,9 +294,51 @@ function writeManagedFile(targetDir, relativePath, newContent, storedChecksums)
180
294
  }
181
295
  return { outcome: "skipped", checksum: storedHash };
182
296
  }
297
+ function writeSectionedFile(targetDir, relativePath, newContent, storedChecksums, overwrite) {
298
+ const fullPath = join3(targetDir, relativePath);
299
+ if (!existsSync3(fullPath)) {
300
+ mkdirSync(dirname2(fullPath), { recursive: true });
301
+ writeFileSync(fullPath, newContent, "utf8");
302
+ const sections2 = parseSections(newContent).map((s) => ({
303
+ id: s.id,
304
+ outcome: "added",
305
+ newChecksum: hashContent(s.inner)
306
+ }));
307
+ return { outcome: "added", checksum: hashContent(newContent), sections: sections2 };
308
+ }
309
+ const existingContent = readFileSync4(fullPath, "utf8");
310
+ if (!hasFences(existingContent)) {
311
+ const currentHash = hashFile(fullPath);
312
+ const storedHash = storedChecksums[relativePath];
313
+ if (!storedHash || currentHash === storedHash) {
314
+ writeFileSync(fullPath, newContent, "utf8");
315
+ const sections2 = parseSections(newContent).map((s) => ({
316
+ id: s.id,
317
+ outcome: "added",
318
+ newChecksum: hashContent(s.inner)
319
+ }));
320
+ return { outcome: "updated", checksum: hashContent(newContent), sections: sections2 };
321
+ } else {
322
+ return { outcome: "skipped", checksum: storedHash ?? currentHash, sections: void 0 };
323
+ }
324
+ }
325
+ const newSectionMap = {};
326
+ for (const s of parseSections(newContent)) {
327
+ newSectionMap[s.id] = s.inner;
328
+ }
329
+ const { content: mergedContent, sections } = mergeSections(
330
+ existingContent,
331
+ newSectionMap,
332
+ storedChecksums,
333
+ relativePath,
334
+ overwrite
335
+ );
336
+ writeFileSync(fullPath, mergedContent, "utf8");
337
+ return { outcome: "updated", checksum: hashContent(mergedContent), sections };
338
+ }
183
339
 
184
340
  // src/state.ts
185
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
341
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
186
342
  import { join as join4 } from "path";
187
343
  var STATE_FILENAME = ".patina-state.json";
188
344
  function normalizeChecksums(checksums) {
@@ -195,7 +351,7 @@ function normalizeChecksums(checksums) {
195
351
  function readState(root, profile) {
196
352
  const statePath = join4(root, STATE_FILENAME);
197
353
  if (existsSync4(statePath)) {
198
- const raw = readFileSync4(statePath, "utf8");
354
+ const raw = readFileSync5(statePath, "utf8");
199
355
  let parsed;
200
356
  try {
201
357
  parsed = JSON.parse(raw);
@@ -242,6 +398,23 @@ function touch(targetDir, relativePath) {
242
398
  mkdirSync2(dirname3(full), { recursive: true });
243
399
  writeFileSync3(full, "", "utf8");
244
400
  }
401
+ var MANIFEST_REQUIRED_FIELDS = ["name", "label", "reflect_hook", "description", "installed"];
402
+ function extractFrontmatter(content) {
403
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
404
+ if (!match) return null;
405
+ const parsed = yaml2.load(match[1]);
406
+ if (typeof parsed !== "object" || parsed === null) return null;
407
+ return parsed;
408
+ }
409
+ function validateManifestFrontmatter(moduleName, content) {
410
+ const fm = extractFrontmatter(content);
411
+ if (!fm) throw new Error(`Module "${moduleName}" manifest has missing or unparseable frontmatter`);
412
+ for (const field of MANIFEST_REQUIRED_FIELDS) {
413
+ if (!fm[field]) {
414
+ throw new Error(`Module "${moduleName}" manifest is missing required field "${field}"`);
415
+ }
416
+ }
417
+ }
245
418
  function profileToVars(profile, liProfileUrl) {
246
419
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
247
420
  return {
@@ -329,9 +502,16 @@ async function scaffold(opts) {
329
502
  ...baseManagedFiles(vars, editor, targetDir),
330
503
  ...modules.flatMap((m) => moduleManagedFiles(m, vars))
331
504
  ];
505
+ for (const module of modules) {
506
+ const manifestEntry = managedFiles.find(([p2]) => p2 === `.claude/modules/${module}/manifest.md`);
507
+ if (manifestEntry) validateManifestFrontmatter(module, manifestEntry[1]);
508
+ }
332
509
  for (const [relativePath, content] of managedFiles) {
333
- const { checksum } = writeManagedFile(targetDir, relativePath, content, {});
334
- checksums[relativePath] = checksum;
510
+ const result = writeManagedFile(targetDir, relativePath, content, {});
511
+ checksums[relativePath] = result.checksum;
512
+ for (const s of result.sections ?? []) {
513
+ checksums[`${relativePath}:${s.id}`] = s.newChecksum;
514
+ }
335
515
  }
336
516
  const baseDirs = ["notes", "skills", "posts"];
337
517
  for (const dir of baseDirs) {
@@ -353,7 +533,7 @@ ${STATE_FILENAME}
353
533
  }
354
534
 
355
535
  // src/validate.ts
356
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync } from "fs";
536
+ import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync } from "fs";
357
537
  import { join as join6, relative, sep, basename } from "path";
358
538
  var NOTES = CONTENT_SUBDIRS[0];
359
539
  var SKILLS = CONTENT_SUBDIRS[1];
@@ -425,7 +605,7 @@ function checkSkillNotes(root, profile) {
425
605
  const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
426
606
  const issues = [];
427
607
  for (const skillFile of listMarkdownFiles(skillsDir)) {
428
- const content = readFileSync5(skillFile, "utf8");
608
+ const content = readFileSync6(skillFile, "utf8");
429
609
  const links = extractWikiLinks(content);
430
610
  for (const { target, line } of links) {
431
611
  if (!noteSlugs.has(target)) {
@@ -452,7 +632,7 @@ function checkWikiLinks(root, profile) {
452
632
  ...listMarkdownFiles(postsDir)
453
633
  ];
454
634
  for (const file of filesToScan) {
455
- const content = readFileSync5(file, "utf8");
635
+ const content = readFileSync6(file, "utf8");
456
636
  const links = extractWikiLinks(content);
457
637
  for (const { target, line } of links) {
458
638
  if (!noteSlugs.has(target)) {
@@ -472,7 +652,7 @@ function checkExclusions(root, profile) {
472
652
  const notesDir = join6(contentDir, NOTES);
473
653
  const exclusionsPath = join6(notesDir, "exclusions.md");
474
654
  if (!existsSync5(exclusionsPath)) return [];
475
- const content = readFileSync5(exclusionsPath, "utf8");
655
+ const content = readFileSync6(exclusionsPath, "utf8");
476
656
  const items = parseExclusions(content);
477
657
  if (items.length === 0) return [];
478
658
  const skillsDir = join6(contentDir, SKILLS);
@@ -484,7 +664,7 @@ function checkExclusions(root, profile) {
484
664
  const issues = [];
485
665
  const seen = /* @__PURE__ */ new Set();
486
666
  for (const file of filesToScan) {
487
- const fileContent = readFileSync5(file, "utf8");
667
+ const fileContent = readFileSync6(file, "utf8");
488
668
  const lines = fileContent.split("\n");
489
669
  for (let i = 0; i < lines.length; i++) {
490
670
  const lineText = lines[i];
@@ -721,6 +901,15 @@ function writeProfile(cwd, profile) {
721
901
  function removeManagedFileIfUnmodified(targetDir, rel, stored) {
722
902
  const fullPath = join7(targetDir, rel);
723
903
  if (!existsSync6(fullPath)) return "deleted";
904
+ const fileContent = readFileSync7(fullPath, "utf8");
905
+ if (hasFences(fileContent)) {
906
+ const editedIds = inspectSections(rel, fileContent, stored);
907
+ if (editedIds.length > 0) {
908
+ return "kept";
909
+ }
910
+ unlinkSync(fullPath);
911
+ return "deleted";
912
+ }
724
913
  const currentHash = hashFile(fullPath);
725
914
  const storedHash = stored[rel];
726
915
  if (storedHash && currentHash !== storedHash) {
@@ -762,7 +951,7 @@ async function runUpdate(cwd) {
762
951
  await runValidate(cwd, profile);
763
952
  }
764
953
  }
765
- function applyProfileUpdate(cwd, profile, fields) {
954
+ function applyProfileUpdate(cwd, profile, fields, overwrite) {
766
955
  const updatedProfile = {
767
956
  ...profile,
768
957
  name: fields.name.trim(),
@@ -785,10 +974,19 @@ function applyProfileUpdate(cwd, profile, fields) {
785
974
  ];
786
975
  const updated = [];
787
976
  const skipped = [];
977
+ const keptSections = [];
788
978
  for (const [rel, content] of files) {
789
- const { outcome, checksum } = writeManagedFile(cwd, rel, content, stored);
790
- newChecksums[rel] = checksum;
791
- if (outcome === "skipped") {
979
+ const result = writeManagedFile(cwd, rel, content, stored, overwrite);
980
+ newChecksums[rel] = result.checksum;
981
+ for (const s of result.sections ?? []) {
982
+ const sKey = `${rel}:${s.id}`;
983
+ if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
984
+ else {
985
+ newChecksums[sKey] = stored[sKey] ?? "";
986
+ keptSections.push(sKey);
987
+ }
988
+ }
989
+ if (result.outcome === "skipped") {
792
990
  skipped.push(rel);
793
991
  } else {
794
992
  updated.push(rel);
@@ -802,7 +1000,7 @@ function applyProfileUpdate(cwd, profile, fields) {
802
1000
  writeState(cwd, { checksums: newChecksums });
803
1001
  const profileToWrite = stripLegacyChecksums(updatedProfile);
804
1002
  writeProfile(cwd, profileToWrite);
805
- return { profile: profileToWrite, updated, skipped };
1003
+ return { profile: profileToWrite, updated, skipped, keptSections };
806
1004
  }
807
1005
  async function runUpdateProfile(cwd, profile) {
808
1006
  console.log("");
@@ -855,7 +1053,7 @@ async function runUpdateProfile(cwd, profile) {
855
1053
  },
856
1054
  { onCancel }
857
1055
  );
858
- const { updated, skipped } = applyProfileUpdate(cwd, profile, {
1056
+ const fields = {
859
1057
  name: identity.name,
860
1058
  title: identity.title ?? "",
861
1059
  roleDescription: identity.roleDescription ?? "",
@@ -864,11 +1062,52 @@ async function runUpdateProfile(cwd, profile) {
864
1062
  companyName: work.companyName ?? "",
865
1063
  website: work.website ?? "",
866
1064
  companyDescription: work.companyDescription ?? ""
867
- });
1065
+ };
1066
+ const overwriteSet = /* @__PURE__ */ new Set();
1067
+ const previewProfile = {
1068
+ ...profile,
1069
+ name: fields.name.trim(),
1070
+ title: fields.title.trim(),
1071
+ role_description: fields.roleDescription.trim() || void 0,
1072
+ job_description_url: fields.jobDescriptionUrl.trim() || void 0,
1073
+ work: {
1074
+ self_employed: fields.selfEmployed,
1075
+ company_name: fields.companyName.trim() || (fields.selfEmployed ? "Freelance" : ""),
1076
+ website: fields.website.trim() || void 0,
1077
+ company_description: fields.companyDescription.trim() || void 0
1078
+ }
1079
+ };
1080
+ const previewVars = profileToVars(previewProfile);
1081
+ const storedChecksums = readState(cwd, profile).checksums;
1082
+ const previewFiles = [
1083
+ ...baseManagedFiles(previewVars, previewProfile.editor, cwd),
1084
+ ...previewProfile.modules.flatMap((m) => moduleManagedFiles(m, previewVars))
1085
+ ];
1086
+ for (const [rel, content] of previewFiles) {
1087
+ if (hasFences(content)) {
1088
+ const fullPath = join7(cwd, rel);
1089
+ if (existsSync6(fullPath)) {
1090
+ const existingContent = readFileSync7(fullPath, "utf8");
1091
+ const editedIds = inspectSections(rel, existingContent, storedChecksums);
1092
+ for (const sectionId of editedIds) {
1093
+ const confirmed = await p.confirm({
1094
+ message: `Section '${sectionId}' in ${rel} has been manually edited. Overwrite?`,
1095
+ initialValue: false
1096
+ });
1097
+ if (p.isCancel(confirmed)) onCancel();
1098
+ if (confirmed) overwriteSet.add(sectionId);
1099
+ }
1100
+ }
1101
+ }
1102
+ }
1103
+ const { updated, skipped, keptSections } = applyProfileUpdate(cwd, profile, fields, overwriteSet);
868
1104
  const summaryLines = [];
869
1105
  if (updated.length > 0) {
870
1106
  summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
871
1107
  }
1108
+ if (keptSections.length > 0) {
1109
+ summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSections.join(", ")}`));
1110
+ }
872
1111
  if (skipped.length > 0) {
873
1112
  summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skipped.join(", ")}`));
874
1113
  }
@@ -883,6 +1122,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
883
1122
  const skippedFiles = [];
884
1123
  const deleted = [];
885
1124
  const kept = [];
1125
+ const keptSections = [];
886
1126
  for (const module of toAdd) {
887
1127
  const def = getModule(module);
888
1128
  if (def?.onAdd) {
@@ -891,9 +1131,17 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
891
1131
  const vars = profileToVars(updatedProfile);
892
1132
  const contentDir = updatedProfile.content_dir;
893
1133
  for (const [rel, content] of moduleManagedFiles(module, vars)) {
894
- const { outcome, checksum } = writeManagedFile(cwd, rel, content, newChecksums);
895
- newChecksums[rel] = checksum;
896
- if (outcome === "skipped") {
1134
+ const result = writeManagedFile(cwd, rel, content, newChecksums);
1135
+ newChecksums[rel] = result.checksum;
1136
+ for (const s of result.sections ?? []) {
1137
+ const sKey = `${rel}:${s.id}`;
1138
+ if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
1139
+ else {
1140
+ newChecksums[sKey] = newChecksums[sKey] ?? "";
1141
+ keptSections.push(sKey);
1142
+ }
1143
+ }
1144
+ if (result.outcome === "skipped") {
897
1145
  skippedFiles.push(rel);
898
1146
  } else {
899
1147
  added.push(rel);
@@ -919,6 +1167,10 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
919
1167
  if (result === "deleted") {
920
1168
  deleted.push(rel);
921
1169
  delete newChecksums[rel];
1170
+ const prefix = rel + ":";
1171
+ for (const key of Object.keys(newChecksums)) {
1172
+ if (key.startsWith(prefix)) delete newChecksums[key];
1173
+ }
922
1174
  } else {
923
1175
  kept.push(rel);
924
1176
  }
@@ -931,7 +1183,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
931
1183
  writeState(cwd, { checksums: newChecksums });
932
1184
  const finalProfile = stripLegacyChecksums(updatedProfile);
933
1185
  writeProfile(cwd, finalProfile);
934
- return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept };
1186
+ return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept, keptSections };
935
1187
  }
936
1188
  async function runUpdateModules(cwd, profile) {
937
1189
  const currentModules = profile.modules ?? [];
@@ -970,9 +1222,10 @@ async function runUpdateModules(cwd, profile) {
970
1222
  liProfileUrl = url.trim();
971
1223
  }
972
1224
  }
973
- const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
1225
+ const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles, keptSections: keptSectionKeys } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
974
1226
  const summaryLines = [];
975
1227
  if (addedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Added: ${addedFiles.join(", ")}`));
1228
+ if (keptSectionKeys.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSectionKeys.join(", ")}`));
976
1229
  if (skippedFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skippedFiles.join(", ")}`));
977
1230
  if (deletedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Removed: ${deletedFiles.join(", ")}`));
978
1231
  if (keptFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edited files: ${keptFiles.join(", ")}`));
@@ -1001,7 +1254,7 @@ function registerCommands(_program) {
1001
1254
  // src/cli.ts
1002
1255
  import chalk2 from "chalk";
1003
1256
  var program = new Command();
1004
- program.name("patina").description("Personal professional knowledge graph \u2014 setup and management");
1257
+ program.name("patina").description("Personal professional knowledge graph \u2014 setup and management").allowExcessArguments(true);
1005
1258
  program.command("validate").description("Check your patina for broken links and excluded items").action(() => {
1006
1259
  try {
1007
1260
  const cwd = process.cwd();
@@ -2,6 +2,7 @@
2
2
 
3
3
  This file is loaded automatically each session to give you context about who you're working with and how this patina is organised.
4
4
 
5
+ <!-- patina:profile:start -->
5
6
  ## Who you're working with
6
7
 
7
8
  **Name:** {{USER_NAME}}
@@ -11,6 +12,7 @@ This file is loaded automatically each session to give you context about who you
11
12
  {{ROLE_DESCRIPTION}}
12
13
 
13
14
  {{COMPANY_DESCRIPTION}}
15
+ <!-- patina:profile:end -->
14
16
 
15
17
  ## What patina is
16
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-patina",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Personal professional knowledge graph — setup and management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@clack/prompts": "^0.9.0",
22
22
  "chalk": "^5.3.0",
23
- "commander": "^12.1.0",
23
+ "commander": "^15.0.0",
24
24
  "js-yaml": "^4.1.0"
25
25
  },
26
26
  "devDependencies": {
@@ -28,8 +28,8 @@
28
28
  "@types/node": "^20.0.0",
29
29
  "tsup": "^8.0.0",
30
30
  "tsx": "^4.0.0",
31
- "typescript": "^5.0.0",
32
- "vitest": "^1.0.0"
31
+ "typescript": "^6.0.3",
32
+ "vitest": "^4.1.7"
33
33
  },
34
34
  "tsup": {
35
35
  "entry": [