my-patina 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/dist/cli.js +266 -227
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/wizard.ts
4
4
  import * as p from "@clack/prompts";
5
5
  import chalk from "chalk";
6
- import { dirname as dirname3, join as join5, resolve } from "path";
6
+ import { dirname as dirname4, join as join6, resolve } from "path";
7
7
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8
8
  import yaml3 from "js-yaml";
9
9
 
@@ -20,9 +20,8 @@ function loadProfile(root) {
20
20
  }
21
21
 
22
22
  // src/scaffold.ts
23
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
24
- import { join as join3, dirname as dirname2 } from "path";
25
- import { fileURLToPath } from "url";
23
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
24
+ import { join as join4, dirname as dirname3 } from "path";
26
25
  import yaml2 from "js-yaml";
27
26
 
28
27
  // src/template.ts
@@ -34,65 +33,139 @@ function render(template, vars) {
34
33
 
35
34
  // src/upgrade.ts
36
35
  import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
37
- import { join as join2, dirname } from "path";
36
+ import { join as join3, dirname as dirname2 } from "path";
38
37
 
39
38
  // src/checksums.ts
40
39
  import { createHash } from "crypto";
41
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
40
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
41
+
42
+ // src/template-loader.ts
43
+ import { readFileSync as readFileSync2 } from "fs";
44
+ import { join as join2, dirname } from "path";
45
+ import { fileURLToPath } from "url";
46
+ var __dirname = dirname(fileURLToPath(import.meta.url));
47
+ var TEMPLATES_DIR = join2(__dirname, "templates");
48
+ function tpl(relativePath) {
49
+ return readFileSync2(join2(TEMPLATES_DIR, relativePath), "utf8");
50
+ }
51
+
52
+ // src/modules/linkedin/index.ts
53
+ var LI_COMMANDS = [
54
+ "li-all.md",
55
+ "li-about.md",
56
+ "li-headline.md",
57
+ "li-experience.md",
58
+ "li-skills.md",
59
+ "li-featured.md",
60
+ "li-activity.md"
61
+ ];
62
+ var LI_MANAGED_PATHS = [
63
+ ...LI_COMMANDS.map((c) => `.claude/commands/${c}`),
64
+ ".claude/modules/linkedin/manifest.md"
65
+ ];
66
+ var CONTENT_FILE_NAMES = [
67
+ "INSTRUCTIONS.md",
68
+ "LinkedIn Current State.md",
69
+ "LinkedIn About.md",
70
+ "LinkedIn Headline.md",
71
+ "LinkedIn Experience.md",
72
+ "LinkedIn Skills.md",
73
+ "LinkedIn Featured.md",
74
+ "LinkedIn Activity.md"
75
+ ];
76
+ var linkedinModule = {
77
+ id: "linkedin",
78
+ label: "LinkedIn",
79
+ hint: "draft and refine your LinkedIn profile",
80
+ managedPaths: LI_MANAGED_PATHS,
81
+ contentFileNames: CONTENT_FILE_NAMES,
82
+ managedFiles(vars) {
83
+ const files = LI_COMMANDS.map((cmd2) => [
84
+ `.claude/commands/${cmd2}`,
85
+ render(tpl(`modules/linkedin/commands/${cmd2}`), vars)
86
+ ]);
87
+ files.push([
88
+ ".claude/modules/linkedin/manifest.md",
89
+ render(tpl("modules/linkedin/manifest.md"), vars)
90
+ ]);
91
+ return files;
92
+ },
93
+ contentFiles(vars, contentDir) {
94
+ return CONTENT_FILE_NAMES.map((file) => [
95
+ `${contentDir}/linkedin/${file}`,
96
+ render(tpl(`modules/linkedin/graph/${file}`), vars)
97
+ ]);
98
+ },
99
+ onAdd(profile, inputs) {
100
+ if (!profile.linkedin?.profile_url && inputs.liProfileUrl?.trim()) {
101
+ return { ...profile, linkedin: { profile_url: inputs.liProfileUrl.trim() } };
102
+ }
103
+ return profile;
104
+ },
105
+ onRemove(profile) {
106
+ const updated = { ...profile };
107
+ delete updated.linkedin;
108
+ return updated;
109
+ }
110
+ };
111
+
112
+ // src/modules/resume/index.ts
113
+ var RESUME_MANAGED_PATHS = [
114
+ ".claude/commands/resume-refresh.md",
115
+ ".claude/modules/resume/manifest.md"
116
+ ];
117
+ var CONTENT_FILE_NAMES2 = [
118
+ "INSTRUCTIONS.md",
119
+ "Resume Working Draft.md",
120
+ "Resume Last Submitted.md"
121
+ ];
122
+ var resumeModule = {
123
+ id: "resume",
124
+ label: "Resume",
125
+ hint: "keep your resume current from your graph",
126
+ managedPaths: RESUME_MANAGED_PATHS,
127
+ contentFileNames: CONTENT_FILE_NAMES2,
128
+ managedFiles(vars) {
129
+ const [commandPath, manifestPath] = RESUME_MANAGED_PATHS;
130
+ return [
131
+ [commandPath, render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
132
+ [manifestPath, render(tpl("modules/resume/manifest.md"), vars)]
133
+ ];
134
+ },
135
+ contentFiles(vars, contentDir) {
136
+ return CONTENT_FILE_NAMES2.map((file) => [
137
+ `${contentDir}/resume/${file}`,
138
+ render(tpl(`modules/resume/graph/${file}`), vars)
139
+ ]);
140
+ }
141
+ // Resume has no module-specific profile fields — no onAdd/onRemove needed.
142
+ };
143
+
144
+ // src/modules/registry.ts
145
+ var MODULES = [linkedinModule, resumeModule];
146
+ var BY_ID = new Map(MODULES.map((m) => [m.id, m]));
147
+ function getModule(id) {
148
+ return BY_ID.get(id);
149
+ }
150
+
151
+ // src/checksums.ts
42
152
  function hashContent(content) {
43
153
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
44
154
  }
45
155
  function hashFile(filePath) {
46
156
  if (!existsSync2(filePath)) return null;
47
- return hashContent(readFileSync2(filePath, "utf8"));
157
+ return hashContent(readFileSync3(filePath, "utf8"));
48
158
  }
49
159
  var CONTENT_SUBDIRS = ["notes", "skills", "posts"];
50
- var LINKEDIN_MANAGED_FILES = [
51
- ".claude/commands/li-all.md",
52
- ".claude/commands/li-about.md",
53
- ".claude/commands/li-headline.md",
54
- ".claude/commands/li-experience.md",
55
- ".claude/commands/li-skills.md",
56
- ".claude/commands/li-featured.md",
57
- ".claude/commands/li-activity.md"
58
- ];
59
- var RESUME_MANAGED_FILES = [
60
- ".claude/commands/resume-refresh.md"
61
- ];
62
- var MODULE_MANAGED_FILES = {
63
- linkedin: [
64
- ...LINKEDIN_MANAGED_FILES,
65
- ".claude/modules/linkedin/manifest.md"
66
- ],
67
- resume: [
68
- ...RESUME_MANAGED_FILES,
69
- ".claude/modules/resume/manifest.md"
70
- ]
71
- };
72
- var MODULE_CONTENT_FILES = {
73
- linkedin: [
74
- "INSTRUCTIONS.md",
75
- "LinkedIn Current State.md",
76
- "LinkedIn About.md",
77
- "LinkedIn Headline.md",
78
- "LinkedIn Experience.md",
79
- "LinkedIn Skills.md",
80
- "LinkedIn Featured.md",
81
- "LinkedIn Activity.md"
82
- ],
83
- resume: [
84
- "INSTRUCTIONS.md",
85
- "Resume Working Draft.md",
86
- "Resume Last Submitted.md"
87
- ]
88
- };
160
+ var MODULE_MANAGED_FILES = Object.fromEntries(MODULES.map((m) => [m.id, m.managedPaths]));
161
+ var MODULE_CONTENT_FILES = Object.fromEntries(MODULES.map((m) => [m.id, m.contentFileNames]));
89
162
 
90
163
  // src/upgrade.ts
91
164
  function writeManagedFile(targetDir, relativePath, newContent, storedChecksums) {
92
- const fullPath = join2(targetDir, relativePath);
165
+ const fullPath = join3(targetDir, relativePath);
93
166
  const newChecksum = hashContent(newContent);
94
167
  if (!existsSync3(fullPath)) {
95
- mkdirSync(dirname(fullPath), { recursive: true });
168
+ mkdirSync(dirname2(fullPath), { recursive: true });
96
169
  writeFileSync(fullPath, newContent, "utf8");
97
170
  return { outcome: "added", checksum: newChecksum };
98
171
  }
@@ -106,19 +179,14 @@ function writeManagedFile(targetDir, relativePath, newContent, storedChecksums)
106
179
  }
107
180
 
108
181
  // src/scaffold.ts
109
- var __dirname = dirname2(fileURLToPath(import.meta.url));
110
- var TEMPLATES_DIR = join3(__dirname, "templates");
111
- function tpl(relativePath) {
112
- return readFileSync3(join3(TEMPLATES_DIR, relativePath), "utf8");
113
- }
114
182
  function writeRaw(targetDir, relativePath, content) {
115
- const full = join3(targetDir, relativePath);
116
- mkdirSync2(dirname2(full), { recursive: true });
183
+ const full = join4(targetDir, relativePath);
184
+ mkdirSync2(dirname3(full), { recursive: true });
117
185
  writeFileSync2(full, content, "utf8");
118
186
  }
119
187
  function touch(targetDir, relativePath) {
120
- const full = join3(targetDir, relativePath);
121
- mkdirSync2(dirname2(full), { recursive: true });
188
+ const full = join4(targetDir, relativePath);
189
+ mkdirSync2(dirname3(full), { recursive: true });
122
190
  writeFileSync2(full, "", "utf8");
123
191
  }
124
192
  function profileToVars(profile, liProfileUrl) {
@@ -148,7 +216,7 @@ function baseManagedFiles(vars, editor, targetDir) {
148
216
  mcpServers: {
149
217
  obsidian: {
150
218
  command: "npx",
151
- args: ["-y", "mcp-obsidian@latest", join3(targetDir, vars.CONTENT_DIR).replace(/\\/g, "/")]
219
+ args: ["-y", "mcp-obsidian@latest", join4(targetDir, vars.CONTENT_DIR).replace(/\\/g, "/")]
152
220
  }
153
221
  }
154
222
  };
@@ -157,50 +225,10 @@ function baseManagedFiles(vars, editor, targetDir) {
157
225
  return files;
158
226
  }
159
227
  function moduleManagedFiles(module, vars) {
160
- if (module === "linkedin") {
161
- const liCmds = [
162
- "li-all.md",
163
- "li-about.md",
164
- "li-headline.md",
165
- "li-experience.md",
166
- "li-skills.md",
167
- "li-featured.md",
168
- "li-activity.md"
169
- ];
170
- const files = liCmds.map((cmd2) => [
171
- `.claude/commands/${cmd2}`,
172
- render(tpl(`modules/linkedin/commands/${cmd2}`), vars)
173
- ]);
174
- files.push([
175
- ".claude/modules/linkedin/manifest.md",
176
- render(tpl("modules/linkedin/manifest.md"), vars)
177
- ]);
178
- return files;
179
- }
180
- if (module === "resume") {
181
- return [
182
- [".claude/commands/resume-refresh.md", render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
183
- [".claude/modules/resume/manifest.md", render(tpl("modules/resume/manifest.md"), vars)]
184
- ];
185
- }
186
- return [];
228
+ return getModule(module)?.managedFiles(vars) ?? [];
187
229
  }
188
230
  function moduleContentFiles(module, vars, contentDir) {
189
- if (module === "linkedin") {
190
- const files = MODULE_CONTENT_FILES["linkedin"] ?? [];
191
- return files.map((file) => [
192
- `${contentDir}/linkedin/${file}`,
193
- render(tpl(`modules/linkedin/graph/${file}`), vars)
194
- ]);
195
- }
196
- if (module === "resume") {
197
- const files = MODULE_CONTENT_FILES["resume"] ?? [];
198
- return files.map((file) => [
199
- `${contentDir}/resume/${file}`,
200
- render(tpl(`modules/resume/graph/${file}`), vars)
201
- ]);
202
- }
203
- return [];
231
+ return getModule(module)?.contentFiles(vars, contentDir) ?? [];
204
232
  }
205
233
  async function scaffold(opts) {
206
234
  const {
@@ -273,18 +301,18 @@ async function scaffold(opts) {
273
301
 
274
302
  // src/validate.ts
275
303
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "fs";
276
- import { join as join4, relative, sep, basename } from "path";
304
+ import { join as join5, relative, sep, basename } from "path";
277
305
  var NOTES = CONTENT_SUBDIRS[0];
278
306
  var SKILLS = CONTENT_SUBDIRS[1];
279
307
  var POSTS = CONTENT_SUBDIRS[2];
280
308
  function findPatinaRoot(cwd) {
281
- return existsSync4(join4(cwd, "profile.yaml")) ? cwd : null;
309
+ return existsSync4(join5(cwd, "profile.yaml")) ? cwd : null;
282
310
  }
283
311
  function listMarkdownFiles(dir) {
284
312
  if (!existsSync4(dir)) return [];
285
313
  const results = [];
286
314
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
287
- const fullPath = join4(dir, entry.name);
315
+ const fullPath = join5(dir, entry.name);
288
316
  if (entry.isDirectory()) {
289
317
  results.push(...listMarkdownFiles(fullPath));
290
318
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -337,9 +365,9 @@ function parseExclusions(markdown) {
337
365
  return [...new Set(items)];
338
366
  }
339
367
  function checkSkillNotes(root, profile) {
340
- const contentDir = join4(root, profile.content_dir ?? "graph");
341
- const notesDir = join4(contentDir, NOTES);
342
- const skillsDir = join4(contentDir, SKILLS);
368
+ const contentDir = join5(root, profile.content_dir ?? "graph");
369
+ const notesDir = join5(contentDir, NOTES);
370
+ const skillsDir = join5(contentDir, SKILLS);
343
371
  const noteFiles = listMarkdownFiles(notesDir);
344
372
  const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
345
373
  const issues = [];
@@ -360,9 +388,9 @@ function checkSkillNotes(root, profile) {
360
388
  return issues;
361
389
  }
362
390
  function checkWikiLinks(root, profile) {
363
- const contentDir = join4(root, profile.content_dir ?? "graph");
364
- const notesDir = join4(contentDir, NOTES);
365
- const postsDir = join4(contentDir, POSTS);
391
+ const contentDir = join5(root, profile.content_dir ?? "graph");
392
+ const notesDir = join5(contentDir, NOTES);
393
+ const postsDir = join5(contentDir, POSTS);
366
394
  const noteFiles = listMarkdownFiles(notesDir);
367
395
  const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
368
396
  const issues = [];
@@ -387,15 +415,15 @@ function checkWikiLinks(root, profile) {
387
415
  return issues;
388
416
  }
389
417
  function checkExclusions(root, profile) {
390
- const contentDir = join4(root, profile.content_dir ?? "graph");
391
- const notesDir = join4(contentDir, NOTES);
392
- const exclusionsPath = join4(notesDir, "exclusions.md");
418
+ const contentDir = join5(root, profile.content_dir ?? "graph");
419
+ const notesDir = join5(contentDir, NOTES);
420
+ const exclusionsPath = join5(notesDir, "exclusions.md");
393
421
  if (!existsSync4(exclusionsPath)) return [];
394
422
  const content = readFileSync4(exclusionsPath, "utf8");
395
423
  const items = parseExclusions(content);
396
424
  if (items.length === 0) return [];
397
- const skillsDir = join4(contentDir, SKILLS);
398
- const postsDir = join4(contentDir, POSTS);
425
+ const skillsDir = join5(contentDir, SKILLS);
426
+ const postsDir = join5(contentDir, POSTS);
399
427
  const filesToScan = [
400
428
  ...listMarkdownFiles(skillsDir),
401
429
  ...listMarkdownFiles(postsDir)
@@ -436,11 +464,11 @@ function validate(root, profile) {
436
464
  if (a.file > b.file) return 1;
437
465
  return (a.line ?? 0) - (b.line ?? 0);
438
466
  });
439
- const contentDir = join4(root, profile.content_dir ?? "graph");
467
+ const contentDir = join5(root, profile.content_dir ?? "graph");
440
468
  const scannedFiles = /* @__PURE__ */ new Set([
441
- ...listMarkdownFiles(join4(contentDir, NOTES)),
442
- ...listMarkdownFiles(join4(contentDir, SKILLS)),
443
- ...listMarkdownFiles(join4(contentDir, POSTS))
469
+ ...listMarkdownFiles(join5(contentDir, NOTES)),
470
+ ...listMarkdownFiles(join5(contentDir, SKILLS)),
471
+ ...listMarkdownFiles(join5(contentDir, POSTS))
444
472
  ]);
445
473
  return {
446
474
  ok: allIssues.length === 0,
@@ -574,18 +602,11 @@ async function runInstall(cwd) {
574
602
  }),
575
603
  modules: () => p.multiselect({
576
604
  message: `Which modules do you want to add?${MULTISELECT_HINT}`,
577
- options: [
578
- {
579
- value: "linkedin",
580
- label: "LinkedIn",
581
- hint: chalk.hex("#64748B")("draft and refine your LinkedIn profile")
582
- },
583
- {
584
- value: "resume",
585
- label: "Resume",
586
- hint: chalk.hex("#64748B")("keep your resume current from your graph")
587
- }
588
- ],
605
+ options: MODULES.map((m) => ({
606
+ value: m.id,
607
+ label: m.label,
608
+ hint: chalk.hex("#64748B")(m.hint)
609
+ })),
589
610
  required: false
590
611
  })
591
612
  },
@@ -641,11 +662,11 @@ async function runInstall(cwd) {
641
662
  p.outro(chalk.hex("#94A3B8")("Run claude from inside your patina to get started."));
642
663
  }
643
664
  function writeProfile(cwd, profile) {
644
- const full = join5(cwd, "profile.yaml");
665
+ const full = join6(cwd, "profile.yaml");
645
666
  writeFileSync3(full, yaml3.dump(profile), "utf8");
646
667
  }
647
668
  function removeManagedFileIfUnmodified(targetDir, rel, stored) {
648
- const fullPath = join5(targetDir, rel);
669
+ const fullPath = join6(targetDir, rel);
649
670
  if (!existsSync5(fullPath)) return "deleted";
650
671
  const currentHash = hashFile(fullPath);
651
672
  const storedHash = stored[rel];
@@ -688,6 +709,47 @@ async function runUpdate(cwd) {
688
709
  await runValidate(cwd, profile);
689
710
  }
690
711
  }
712
+ function applyProfileUpdate(cwd, profile, fields) {
713
+ const updatedProfile = {
714
+ ...profile,
715
+ name: fields.name.trim(),
716
+ title: fields.title.trim(),
717
+ role_description: fields.roleDescription.trim() || void 0,
718
+ job_description_url: fields.jobDescriptionUrl.trim() || void 0,
719
+ work: {
720
+ self_employed: fields.selfEmployed,
721
+ company_name: fields.companyName.trim() || (fields.selfEmployed ? "Freelance" : ""),
722
+ website: fields.website.trim() || void 0,
723
+ company_description: fields.companyDescription.trim() || void 0
724
+ }
725
+ };
726
+ const vars = profileToVars(updatedProfile);
727
+ const stored = profile._checksums ?? {};
728
+ const newChecksums = {};
729
+ const files = [
730
+ ...baseManagedFiles(vars, updatedProfile.editor, cwd),
731
+ ...updatedProfile.modules.flatMap((m) => moduleManagedFiles(m, vars))
732
+ ];
733
+ const updated = [];
734
+ const skipped = [];
735
+ for (const [rel, content] of files) {
736
+ const { outcome, checksum } = writeManagedFile(cwd, rel, content, stored);
737
+ newChecksums[rel] = checksum;
738
+ if (outcome === "skipped") {
739
+ skipped.push(rel);
740
+ } else {
741
+ updated.push(rel);
742
+ }
743
+ }
744
+ for (const [rel, hash] of Object.entries(stored)) {
745
+ if (!(rel in newChecksums)) {
746
+ newChecksums[rel] = hash;
747
+ }
748
+ }
749
+ updatedProfile._checksums = newChecksums;
750
+ writeProfile(cwd, updatedProfile);
751
+ return { profile: updatedProfile, updated, skipped };
752
+ }
691
753
  async function runUpdateProfile(cwd, profile) {
692
754
  console.log("");
693
755
  console.log(` ${label("Update personal info")}`);
@@ -739,44 +801,16 @@ async function runUpdateProfile(cwd, profile) {
739
801
  },
740
802
  { onCancel }
741
803
  );
742
- const updatedProfile = {
743
- ...profile,
744
- name: identity.name.trim(),
745
- title: (identity.title ?? "").trim(),
746
- role_description: (identity.roleDescription ?? "").trim() || void 0,
747
- job_description_url: (identity.jobDescriptionUrl ?? "").trim() || void 0,
748
- work: {
749
- self_employed: selfEmployed,
750
- company_name: work.companyName?.trim() || (selfEmployed ? "Freelance" : ""),
751
- website: work.website?.trim() || void 0,
752
- company_description: work.companyDescription?.trim() || void 0
753
- }
754
- };
755
- const vars = profileToVars(updatedProfile);
756
- const stored = profile._checksums ?? {};
757
- const newChecksums = {};
758
- const files = [
759
- ...baseManagedFiles(vars, updatedProfile.editor, cwd),
760
- ...updatedProfile.modules.flatMap((m) => moduleManagedFiles(m, vars))
761
- ];
762
- const updated = [];
763
- const skipped = [];
764
- for (const [rel, content] of files) {
765
- const { outcome, checksum } = writeManagedFile(cwd, rel, content, stored);
766
- newChecksums[rel] = checksum;
767
- if (outcome === "skipped") {
768
- skipped.push(rel);
769
- } else {
770
- updated.push(rel);
771
- }
772
- }
773
- for (const [rel, hash] of Object.entries(stored)) {
774
- if (!(rel in newChecksums)) {
775
- newChecksums[rel] = hash;
776
- }
777
- }
778
- updatedProfile._checksums = newChecksums;
779
- writeProfile(cwd, updatedProfile);
804
+ const { updated, skipped } = applyProfileUpdate(cwd, profile, {
805
+ name: identity.name,
806
+ title: identity.title ?? "",
807
+ roleDescription: identity.roleDescription ?? "",
808
+ jobDescriptionUrl: identity.jobDescriptionUrl ?? "",
809
+ selfEmployed,
810
+ companyName: work.companyName ?? "",
811
+ website: work.website ?? "",
812
+ companyDescription: work.companyDescription ?? ""
813
+ });
780
814
  const summaryLines = [];
781
815
  if (updated.length > 0) {
782
816
  summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
@@ -787,96 +821,101 @@ async function runUpdateProfile(cwd, profile) {
787
821
  p.note(summaryLines.join("\n"), label("Done"));
788
822
  p.outro(chalk.hex("#94A3B8")("Profile updated."));
789
823
  }
790
- async function runUpdateModules(cwd, profile) {
791
- const currentModules = profile.modules ?? [];
792
- const selected = await p.multiselect({
793
- message: `Which modules do you want active?${MULTISELECT_HINT}`,
794
- options: [
795
- {
796
- value: "linkedin",
797
- label: "LinkedIn",
798
- hint: chalk.hex("#64748B")("draft and refine your LinkedIn profile")
799
- },
800
- {
801
- value: "resume",
802
- label: "Resume",
803
- hint: chalk.hex("#64748B")("keep your resume current from your graph")
804
- }
805
- ],
806
- initialValues: currentModules,
807
- required: false
808
- });
809
- if (p.isCancel(selected)) {
810
- p.cancel(chalk.hex("#94A3B8")("No changes made."));
811
- return;
812
- }
813
- const selectedModules = Array.isArray(selected) ? selected : [];
814
- const toAdd = selectedModules.filter((m) => !currentModules.includes(m));
815
- const toRemove = currentModules.filter((m) => !selectedModules.includes(m));
816
- if (toAdd.length === 0 && toRemove.length === 0) {
817
- p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
818
- return;
819
- }
824
+ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
820
825
  const stored = profile._checksums ?? {};
821
826
  const newChecksums = { ...stored };
822
- const updatedProfile = { ...profile, modules: [...currentModules] };
823
- const addedFiles = [];
827
+ let updatedProfile = { ...profile, modules: [...profile.modules ?? []] };
828
+ const added = [];
824
829
  const skippedFiles = [];
825
- const deletedFiles = [];
826
- const keptFiles = [];
830
+ const deleted = [];
831
+ const kept = [];
827
832
  for (const module of toAdd) {
828
- if (module === "linkedin" && !updatedProfile.linkedin?.profile_url) {
829
- const url = await p.text({
830
- message: "What's your LinkedIn profile URL?",
831
- placeholder: "https://linkedin.com/in/yourname (optional)"
832
- });
833
- if (p.isCancel(url)) {
834
- p.cancel(chalk.hex("#94A3B8")("No changes made."));
835
- return;
836
- }
837
- if (typeof url === "string" && url.trim()) {
838
- updatedProfile.linkedin = { profile_url: url.trim() };
839
- }
833
+ const def = getModule(module);
834
+ if (def?.onAdd) {
835
+ updatedProfile = def.onAdd(updatedProfile, moduleInputs?.[module] ?? {});
840
836
  }
841
837
  const vars = profileToVars(updatedProfile);
842
838
  const contentDir = updatedProfile.content_dir;
843
839
  for (const [rel, content] of moduleManagedFiles(module, vars)) {
844
- const { outcome, checksum } = writeManagedFile(cwd, rel, content, stored);
840
+ const { outcome, checksum } = writeManagedFile(cwd, rel, content, newChecksums);
845
841
  newChecksums[rel] = checksum;
846
842
  if (outcome === "skipped") {
847
843
  skippedFiles.push(rel);
848
844
  } else {
849
- addedFiles.push(rel);
845
+ added.push(rel);
850
846
  }
851
847
  }
852
848
  for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
853
- const fullPath = join5(cwd, relativePath);
849
+ const fullPath = join6(cwd, relativePath);
854
850
  if (!existsSync5(fullPath)) {
855
- mkdirSync3(dirname3(fullPath), { recursive: true });
851
+ mkdirSync3(dirname4(fullPath), { recursive: true });
856
852
  writeFileSync3(fullPath, content, "utf8");
857
- addedFiles.push(relativePath);
853
+ added.push(relativePath);
858
854
  }
859
855
  }
860
- updatedProfile.modules = [...updatedProfile.modules, module];
856
+ if (!updatedProfile.modules.includes(module)) {
857
+ updatedProfile.modules = [...updatedProfile.modules, module];
858
+ }
861
859
  }
862
860
  for (const module of toRemove) {
863
- const managedRels = MODULE_MANAGED_FILES[module] ?? [];
861
+ const def = getModule(module);
862
+ const managedRels = def?.managedPaths ?? [];
864
863
  for (const rel of managedRels) {
865
864
  const result = removeManagedFileIfUnmodified(cwd, rel, stored);
866
865
  if (result === "deleted") {
867
- deletedFiles.push(rel);
866
+ deleted.push(rel);
868
867
  delete newChecksums[rel];
869
868
  } else {
870
- keptFiles.push(rel);
869
+ kept.push(rel);
871
870
  }
872
871
  }
873
872
  updatedProfile.modules = updatedProfile.modules.filter((m) => m !== module);
874
- if (module === "linkedin") {
875
- delete updatedProfile.linkedin;
873
+ if (def?.onRemove) {
874
+ updatedProfile = def.onRemove(updatedProfile);
876
875
  }
877
876
  }
878
877
  updatedProfile._checksums = newChecksums;
879
878
  writeProfile(cwd, updatedProfile);
879
+ return { profile: updatedProfile, added, skipped: skippedFiles, deleted, kept };
880
+ }
881
+ async function runUpdateModules(cwd, profile) {
882
+ const currentModules = profile.modules ?? [];
883
+ const selected = await p.multiselect({
884
+ message: `Which modules do you want active?${MULTISELECT_HINT}`,
885
+ options: MODULES.map((m) => ({
886
+ value: m.id,
887
+ label: m.label,
888
+ hint: chalk.hex("#64748B")(m.hint)
889
+ })),
890
+ initialValues: currentModules,
891
+ required: false
892
+ });
893
+ if (p.isCancel(selected)) {
894
+ p.cancel(chalk.hex("#94A3B8")("No changes made."));
895
+ return;
896
+ }
897
+ const selectedModules = Array.isArray(selected) ? selected : [];
898
+ const toAdd = selectedModules.filter((m) => !currentModules.includes(m));
899
+ const toRemove = currentModules.filter((m) => !selectedModules.includes(m));
900
+ if (toAdd.length === 0 && toRemove.length === 0) {
901
+ p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
902
+ return;
903
+ }
904
+ let liProfileUrl;
905
+ if (toAdd.includes("linkedin") && !profile.linkedin?.profile_url) {
906
+ const url = await p.text({
907
+ message: "What's your LinkedIn profile URL?",
908
+ placeholder: "https://linkedin.com/in/yourname (optional)"
909
+ });
910
+ if (p.isCancel(url)) {
911
+ p.cancel(chalk.hex("#94A3B8")("No changes made."));
912
+ return;
913
+ }
914
+ if (typeof url === "string" && url.trim()) {
915
+ liProfileUrl = url.trim();
916
+ }
917
+ }
918
+ const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
880
919
  const summaryLines = [];
881
920
  if (addedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Added: ${addedFiles.join(", ")}`));
882
921
  if (skippedFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skippedFiles.join(", ")}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-patina",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Personal professional knowledge graph — setup and management",
5
5
  "type": "module",
6
6
  "bin": {