my-patina 0.7.0 → 0.8.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
|
|
10
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
11
11
|
import yaml3 from "js-yaml";
|
|
12
12
|
|
|
13
13
|
// src/detect.ts
|
|
@@ -23,7 +23,7 @@ function loadProfile(root) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// src/scaffold.ts
|
|
26
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
26
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
27
27
|
import { join as join5, dirname as dirname3 } from "path";
|
|
28
28
|
import yaml2 from "js-yaml";
|
|
29
29
|
|
|
@@ -64,7 +64,8 @@ var LI_COMMANDS = [
|
|
|
64
64
|
];
|
|
65
65
|
var LI_MANAGED_PATHS = [
|
|
66
66
|
...LI_COMMANDS.map((c) => `.claude/commands/${c}`),
|
|
67
|
-
".claude/modules/linkedin/manifest.md"
|
|
67
|
+
".claude/modules/linkedin/manifest.md",
|
|
68
|
+
".claude/modules/linkedin/CLAUDE.md"
|
|
68
69
|
];
|
|
69
70
|
var CONTENT_FILE_NAMES = [
|
|
70
71
|
"INSTRUCTIONS.md",
|
|
@@ -91,6 +92,10 @@ var linkedinModule = {
|
|
|
91
92
|
".claude/modules/linkedin/manifest.md",
|
|
92
93
|
render(tpl("modules/linkedin/manifest.md"), vars)
|
|
93
94
|
]);
|
|
95
|
+
files.push([
|
|
96
|
+
".claude/modules/linkedin/CLAUDE.md",
|
|
97
|
+
render(tpl("modules/linkedin/CLAUDE.md"), vars)
|
|
98
|
+
]);
|
|
94
99
|
return files;
|
|
95
100
|
},
|
|
96
101
|
contentFiles(vars, contentDir) {
|
|
@@ -109,12 +114,46 @@ var linkedinModule = {
|
|
|
109
114
|
const updated = { ...profile };
|
|
110
115
|
delete updated.linkedin;
|
|
111
116
|
return updated;
|
|
117
|
+
},
|
|
118
|
+
readmeBlock(vars) {
|
|
119
|
+
return [
|
|
120
|
+
"## LinkedIn module",
|
|
121
|
+
"",
|
|
122
|
+
"Drafts and refines your LinkedIn profile from your graph.",
|
|
123
|
+
"",
|
|
124
|
+
"### Folder additions",
|
|
125
|
+
"",
|
|
126
|
+
"```",
|
|
127
|
+
`${vars.CONTENT_DIR}/linkedin/`,
|
|
128
|
+
" INSTRUCTIONS.md \u2014 module rules and guidance",
|
|
129
|
+
" LinkedIn Current State.md \u2014 your current live profile copy",
|
|
130
|
+
" LinkedIn About.md \u2014 draft for the About section",
|
|
131
|
+
" LinkedIn Headline.md \u2014 draft for your headline",
|
|
132
|
+
" LinkedIn Experience.md \u2014 draft for your experience entries",
|
|
133
|
+
" LinkedIn Skills.md \u2014 draft for your skills section",
|
|
134
|
+
" LinkedIn Featured.md \u2014 draft for featured content",
|
|
135
|
+
" LinkedIn Activity.md \u2014 draft for activity/posts section",
|
|
136
|
+
"```",
|
|
137
|
+
"",
|
|
138
|
+
"### Commands",
|
|
139
|
+
"",
|
|
140
|
+
"| Command | What it does |",
|
|
141
|
+
"|---------|-------------|",
|
|
142
|
+
"| `/li-all` | Run all LinkedIn section drafts in sequence |",
|
|
143
|
+
"| `/li-about` | Draft or refine your LinkedIn About section |",
|
|
144
|
+
"| `/li-headline` | Draft or refine your LinkedIn headline |",
|
|
145
|
+
"| `/li-experience` | Draft or refine your LinkedIn experience entries |",
|
|
146
|
+
"| `/li-skills` | Draft or refine your LinkedIn skills section |",
|
|
147
|
+
"| `/li-featured` | Draft or refine your LinkedIn featured content |",
|
|
148
|
+
"| `/li-activity` | Draft or refine your LinkedIn activity section |"
|
|
149
|
+
].join("\n");
|
|
112
150
|
}
|
|
113
151
|
};
|
|
114
152
|
|
|
115
153
|
// src/modules/resume/index.ts
|
|
116
154
|
var RESUME_MANAGED_PATHS = [
|
|
117
155
|
".claude/commands/resume-refresh.md",
|
|
156
|
+
".claude/modules/resume/CLAUDE.md",
|
|
118
157
|
".claude/modules/resume/manifest.md"
|
|
119
158
|
];
|
|
120
159
|
var CONTENT_FILE_NAMES2 = [
|
|
@@ -129,10 +168,10 @@ var resumeModule = {
|
|
|
129
168
|
managedPaths: RESUME_MANAGED_PATHS,
|
|
130
169
|
contentFileNames: CONTENT_FILE_NAMES2,
|
|
131
170
|
managedFiles(vars) {
|
|
132
|
-
const [commandPath, manifestPath] = RESUME_MANAGED_PATHS;
|
|
133
171
|
return [
|
|
134
|
-
[
|
|
135
|
-
[
|
|
172
|
+
[".claude/commands/resume-refresh.md", render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
|
|
173
|
+
[".claude/modules/resume/CLAUDE.md", render(tpl("modules/resume/CLAUDE.md"), vars)],
|
|
174
|
+
[".claude/modules/resume/manifest.md", render(tpl("modules/resume/manifest.md"), vars)]
|
|
136
175
|
];
|
|
137
176
|
},
|
|
138
177
|
contentFiles(vars, contentDir) {
|
|
@@ -140,6 +179,28 @@ var resumeModule = {
|
|
|
140
179
|
`${contentDir}/resume/${file}`,
|
|
141
180
|
render(tpl(`modules/resume/graph/${file}`), vars)
|
|
142
181
|
]);
|
|
182
|
+
},
|
|
183
|
+
readmeBlock(vars) {
|
|
184
|
+
return [
|
|
185
|
+
"## Resume module",
|
|
186
|
+
"",
|
|
187
|
+
"Keeps your resume current by synthesising it from your graph.",
|
|
188
|
+
"",
|
|
189
|
+
"### Folder additions",
|
|
190
|
+
"",
|
|
191
|
+
"```",
|
|
192
|
+
`${vars.CONTENT_DIR}/resume/`,
|
|
193
|
+
" INSTRUCTIONS.md \u2014 module rules and guidance",
|
|
194
|
+
" Resume Working Draft.md \u2014 the resume you are actively editing",
|
|
195
|
+
" Resume Last Submitted.md \u2014 the version you last sent to an employer",
|
|
196
|
+
"```",
|
|
197
|
+
"",
|
|
198
|
+
"### Commands",
|
|
199
|
+
"",
|
|
200
|
+
"| Command | What it does |",
|
|
201
|
+
"|---------|-------------|",
|
|
202
|
+
"| `/resume-refresh` | Refresh your resume working draft from your graph |"
|
|
203
|
+
].join("\n");
|
|
143
204
|
}
|
|
144
205
|
// Resume has no module-specific profile fields — no onAdd/onRemove needed.
|
|
145
206
|
};
|
|
@@ -258,6 +319,28 @@ function mergeSections(existing, newSections, storedChecksums, relativePath, ove
|
|
|
258
319
|
}
|
|
259
320
|
return { content: result, sections: outcomes };
|
|
260
321
|
}
|
|
322
|
+
function removeSection(id, content) {
|
|
323
|
+
const sections = parseSections(content);
|
|
324
|
+
const section = sections.find((s) => s.id === id);
|
|
325
|
+
if (!section) return content;
|
|
326
|
+
const { start, end } = section;
|
|
327
|
+
let before = content.slice(0, start);
|
|
328
|
+
before = before.replace(/(\r?\n)+$/, "");
|
|
329
|
+
let after = content.slice(end);
|
|
330
|
+
after = after.replace(/^(\r?\n)+/, "");
|
|
331
|
+
let result;
|
|
332
|
+
if (before === "" && after === "") {
|
|
333
|
+
result = "";
|
|
334
|
+
} else if (before === "") {
|
|
335
|
+
result = after;
|
|
336
|
+
} else if (after === "") {
|
|
337
|
+
result = before;
|
|
338
|
+
} else {
|
|
339
|
+
result = before + "\n\n" + after;
|
|
340
|
+
}
|
|
341
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
261
344
|
function inspectSections(relativePath, existing, storedChecksums) {
|
|
262
345
|
const sections = parseSections(existing);
|
|
263
346
|
const editedIds = [];
|
|
@@ -417,6 +500,11 @@ function validateManifestFrontmatter(moduleName, content) {
|
|
|
417
500
|
}
|
|
418
501
|
function profileToVars(profile, liProfileUrl) {
|
|
419
502
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
503
|
+
const modulesSection = (profile.modules ?? []).length ? profile.modules.map((id) => {
|
|
504
|
+
const def = getModule(id);
|
|
505
|
+
const label2 = def?.label ?? id;
|
|
506
|
+
return `- [${label2} module context](.claude/modules/${id}/CLAUDE.md)`;
|
|
507
|
+
}).join("\n") : "_No modules installed._";
|
|
420
508
|
return {
|
|
421
509
|
PATINA_NAME: profile.patina_name,
|
|
422
510
|
USER_NAME: profile.name,
|
|
@@ -431,11 +519,13 @@ function profileToVars(profile, liProfileUrl) {
|
|
|
431
519
|
STALENESS_THRESHOLD: (() => {
|
|
432
520
|
const d = Number(profile.staleness_threshold_days ?? 30);
|
|
433
521
|
return String(Number.isFinite(d) && d > 0 ? d : 30);
|
|
434
|
-
})()
|
|
522
|
+
})(),
|
|
523
|
+
MODULES_SECTION: modulesSection
|
|
435
524
|
};
|
|
436
525
|
}
|
|
437
526
|
function baseManagedFiles(vars, editor, targetDir) {
|
|
438
527
|
const files = [
|
|
528
|
+
["README.md", render(tpl("README.md"), vars)],
|
|
439
529
|
["CLAUDE.md", render(tpl("CLAUDE.md"), vars)],
|
|
440
530
|
[".claude/settings.json", tpl(".claude/settings.json")],
|
|
441
531
|
[".claude/commands/add.md", render(tpl(".claude/commands/add.md"), vars)],
|
|
@@ -491,8 +581,21 @@ async function scaffold(opts) {
|
|
|
491
581
|
const vars = profileToVars(tempProfile, liProfileUrl);
|
|
492
582
|
mkdirSync2(targetDir, { recursive: true });
|
|
493
583
|
const checksums = {};
|
|
584
|
+
const baseFiles = baseManagedFiles(vars, editor, targetDir);
|
|
585
|
+
const readmePath = join5(targetDir, "README.md");
|
|
586
|
+
const filteredBaseFiles = baseFiles.filter(([rel]) => {
|
|
587
|
+
if (rel === "README.md") {
|
|
588
|
+
if (existsSync5(readmePath)) {
|
|
589
|
+
const existing = readFileSync6(readmePath, "utf8");
|
|
590
|
+
if (!hasFences(existing)) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
});
|
|
494
597
|
const managedFiles = [
|
|
495
|
-
...
|
|
598
|
+
...filteredBaseFiles,
|
|
496
599
|
...modules.flatMap((m) => moduleManagedFiles(m, vars))
|
|
497
600
|
];
|
|
498
601
|
for (const module of modules) {
|
|
@@ -506,6 +609,17 @@ async function scaffold(opts) {
|
|
|
506
609
|
checksums[`${relativePath}:${s.id}`] = s.newChecksum;
|
|
507
610
|
}
|
|
508
611
|
}
|
|
612
|
+
for (const module of modules) {
|
|
613
|
+
const def = getModule(module);
|
|
614
|
+
if (def?.readmeBlock) {
|
|
615
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
616
|
+
const result = writeManagedFile(targetDir, "README.md", block, checksums);
|
|
617
|
+
checksums["README.md"] = result.checksum;
|
|
618
|
+
for (const s of result.sections ?? []) {
|
|
619
|
+
checksums[`README.md:${s.id}`] = s.newChecksum;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
509
623
|
const baseDirs = ["notes", "skills", "posts"];
|
|
510
624
|
for (const dir of baseDirs) {
|
|
511
625
|
touch(targetDir, `${contentDir}/${dir}/.gitkeep`);
|
|
@@ -526,16 +640,16 @@ ${STATE_FILENAME}
|
|
|
526
640
|
}
|
|
527
641
|
|
|
528
642
|
// src/validate.ts
|
|
529
|
-
import { existsSync as
|
|
643
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
530
644
|
import { join as join6, relative, sep, basename } from "path";
|
|
531
645
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
532
646
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
533
647
|
var POSTS = CONTENT_SUBDIRS[2];
|
|
534
648
|
function findPatinaRoot(cwd) {
|
|
535
|
-
return
|
|
649
|
+
return existsSync6(join6(cwd, "profile.yaml")) ? cwd : null;
|
|
536
650
|
}
|
|
537
651
|
function listMarkdownFiles(dir) {
|
|
538
|
-
if (!
|
|
652
|
+
if (!existsSync6(dir)) return [];
|
|
539
653
|
const results = [];
|
|
540
654
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
541
655
|
const fullPath = join6(dir, entry.name);
|
|
@@ -598,7 +712,7 @@ function checkSkillNotes(root, profile) {
|
|
|
598
712
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
599
713
|
const issues = [];
|
|
600
714
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
601
|
-
const content =
|
|
715
|
+
const content = readFileSync7(skillFile, "utf8");
|
|
602
716
|
const links = extractWikiLinks(content);
|
|
603
717
|
for (const { target, line } of links) {
|
|
604
718
|
if (!noteSlugs.has(target)) {
|
|
@@ -625,7 +739,7 @@ function checkWikiLinks(root, profile) {
|
|
|
625
739
|
...listMarkdownFiles(postsDir)
|
|
626
740
|
];
|
|
627
741
|
for (const file of filesToScan) {
|
|
628
|
-
const content =
|
|
742
|
+
const content = readFileSync7(file, "utf8");
|
|
629
743
|
const links = extractWikiLinks(content);
|
|
630
744
|
for (const { target, line } of links) {
|
|
631
745
|
if (!noteSlugs.has(target)) {
|
|
@@ -644,8 +758,8 @@ function checkExclusions(root, profile) {
|
|
|
644
758
|
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
645
759
|
const notesDir = join6(contentDir, NOTES);
|
|
646
760
|
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
647
|
-
if (!
|
|
648
|
-
const content =
|
|
761
|
+
if (!existsSync6(exclusionsPath)) return [];
|
|
762
|
+
const content = readFileSync7(exclusionsPath, "utf8");
|
|
649
763
|
const items = parseExclusions(content);
|
|
650
764
|
if (items.length === 0) return [];
|
|
651
765
|
const skillsDir = join6(contentDir, SKILLS);
|
|
@@ -657,7 +771,7 @@ function checkExclusions(root, profile) {
|
|
|
657
771
|
const issues = [];
|
|
658
772
|
const seen = /* @__PURE__ */ new Set();
|
|
659
773
|
for (const file of filesToScan) {
|
|
660
|
-
const fileContent =
|
|
774
|
+
const fileContent = readFileSync7(file, "utf8");
|
|
661
775
|
const lines = fileContent.split("\n");
|
|
662
776
|
for (let i = 0; i < lines.length; i++) {
|
|
663
777
|
const lineText = lines[i];
|
|
@@ -893,8 +1007,8 @@ function writeProfile(cwd, profile) {
|
|
|
893
1007
|
}
|
|
894
1008
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
895
1009
|
const fullPath = join7(targetDir, rel);
|
|
896
|
-
if (!
|
|
897
|
-
const fileContent =
|
|
1010
|
+
if (!existsSync7(fullPath)) return "deleted";
|
|
1011
|
+
const fileContent = readFileSync8(fullPath, "utf8");
|
|
898
1012
|
if (hasFences(fileContent)) {
|
|
899
1013
|
const editedIds = inspectSections(rel, fileContent, stored);
|
|
900
1014
|
if (editedIds.length > 0) {
|
|
@@ -985,6 +1099,32 @@ function applyProfileUpdate(cwd, profile, fields, overwrite) {
|
|
|
985
1099
|
updated.push(rel);
|
|
986
1100
|
}
|
|
987
1101
|
}
|
|
1102
|
+
for (const module of updatedProfile.modules) {
|
|
1103
|
+
const def = getModule(module);
|
|
1104
|
+
if (def?.readmeBlock) {
|
|
1105
|
+
const readmePath = join7(cwd, "README.md");
|
|
1106
|
+
const readmeExists = existsSync7(readmePath);
|
|
1107
|
+
const readmeHasFences = readmeExists ? hasFences(readFileSync8(readmePath, "utf8")) : false;
|
|
1108
|
+
if (!readmeExists || readmeHasFences) {
|
|
1109
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
1110
|
+
const result = writeManagedFile(cwd, "README.md", block, newChecksums, overwrite);
|
|
1111
|
+
newChecksums["README.md"] = result.checksum;
|
|
1112
|
+
for (const s of result.sections ?? []) {
|
|
1113
|
+
const sKey = `README.md:${s.id}`;
|
|
1114
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1115
|
+
else {
|
|
1116
|
+
newChecksums[sKey] = stored[sKey] ?? "";
|
|
1117
|
+
keptSections.push(sKey);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (result.outcome === "skipped") {
|
|
1121
|
+
skipped.push(`README.md:${module}`);
|
|
1122
|
+
} else if (result.outcome !== "updated" || result.sections?.some((s) => s.id === module && s.outcome !== "unchanged")) {
|
|
1123
|
+
updated.push(`README.md:${module}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
988
1128
|
for (const [rel, hash] of Object.entries(stored)) {
|
|
989
1129
|
if (!(rel in newChecksums)) {
|
|
990
1130
|
newChecksums[rel] = hash;
|
|
@@ -1079,8 +1219,8 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
1079
1219
|
for (const [rel, content] of previewFiles) {
|
|
1080
1220
|
if (hasFences(content)) {
|
|
1081
1221
|
const fullPath = join7(cwd, rel);
|
|
1082
|
-
if (
|
|
1083
|
-
const existingContent =
|
|
1222
|
+
if (existsSync7(fullPath)) {
|
|
1223
|
+
const existingContent = readFileSync8(fullPath, "utf8");
|
|
1084
1224
|
const editedIds = inspectSections(rel, existingContent, storedChecksums);
|
|
1085
1225
|
for (const sectionId of editedIds) {
|
|
1086
1226
|
const confirmed = await p.confirm({
|
|
@@ -1142,12 +1282,28 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
1142
1282
|
}
|
|
1143
1283
|
for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
|
|
1144
1284
|
const fullPath = join7(cwd, relativePath);
|
|
1145
|
-
if (!
|
|
1285
|
+
if (!existsSync7(fullPath)) {
|
|
1146
1286
|
mkdirSync3(dirname4(fullPath), { recursive: true });
|
|
1147
1287
|
writeFileSync4(fullPath, content, "utf8");
|
|
1148
1288
|
added.push(relativePath);
|
|
1149
1289
|
}
|
|
1150
1290
|
}
|
|
1291
|
+
if (def?.readmeBlock) {
|
|
1292
|
+
const readmePath = join7(cwd, "README.md");
|
|
1293
|
+
const readmeExists = existsSync7(readmePath);
|
|
1294
|
+
const readmeHasFences = readmeExists ? hasFences(readFileSync8(readmePath, "utf8")) : false;
|
|
1295
|
+
if (!readmeExists || readmeHasFences) {
|
|
1296
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
1297
|
+
const result = writeManagedFile(cwd, "README.md", block, newChecksums);
|
|
1298
|
+
newChecksums["README.md"] = result.checksum;
|
|
1299
|
+
for (const s of result.sections ?? []) {
|
|
1300
|
+
newChecksums[`README.md:${s.id}`] = s.newChecksum;
|
|
1301
|
+
}
|
|
1302
|
+
if (result.outcome !== "skipped") added.push(`README.md:${module}`);
|
|
1303
|
+
} else {
|
|
1304
|
+
kept.push("README.md");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1151
1307
|
if (!updatedProfile.modules.includes(module)) {
|
|
1152
1308
|
updatedProfile.modules = [...updatedProfile.modules, module];
|
|
1153
1309
|
}
|
|
@@ -1168,11 +1324,40 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
1168
1324
|
kept.push(rel);
|
|
1169
1325
|
}
|
|
1170
1326
|
}
|
|
1327
|
+
const readmePath = join7(cwd, "README.md");
|
|
1328
|
+
if (existsSync7(readmePath)) {
|
|
1329
|
+
const before = readFileSync8(readmePath, "utf8");
|
|
1330
|
+
const editedIds = inspectSections("README.md", before, stored);
|
|
1331
|
+
if (!editedIds.includes(module)) {
|
|
1332
|
+
const after = removeSection(module, before);
|
|
1333
|
+
if (after !== before) {
|
|
1334
|
+
writeFileSync4(readmePath, after, "utf8");
|
|
1335
|
+
newChecksums["README.md"] = hashContent(after);
|
|
1336
|
+
delete newChecksums[`README.md:${module}`];
|
|
1337
|
+
deleted.push(`README.md:${module}`);
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1340
|
+
keptSections.push(`README.md:${module}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1171
1343
|
updatedProfile.modules = updatedProfile.modules.filter((m) => m !== module);
|
|
1172
1344
|
if (def?.onRemove) {
|
|
1173
1345
|
updatedProfile = def.onRemove(updatedProfile);
|
|
1174
1346
|
}
|
|
1175
1347
|
}
|
|
1348
|
+
const finalVars = profileToVars(updatedProfile);
|
|
1349
|
+
for (const [rel, content] of baseManagedFiles(finalVars, updatedProfile.editor, cwd)) {
|
|
1350
|
+
const result = writeManagedFile(cwd, rel, content, newChecksums);
|
|
1351
|
+
newChecksums[rel] = result.checksum;
|
|
1352
|
+
for (const s of result.sections ?? []) {
|
|
1353
|
+
const sKey = `${rel}:${s.id}`;
|
|
1354
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1355
|
+
else {
|
|
1356
|
+
newChecksums[sKey] = newChecksums[sKey] ?? "";
|
|
1357
|
+
keptSections.push(sKey);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1176
1361
|
writeState(cwd, { checksums: newChecksums });
|
|
1177
1362
|
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
1178
1363
|
writeProfile(cwd, finalProfile);
|
|
@@ -1201,6 +1386,16 @@ async function runUpdateModules(cwd, profile) {
|
|
|
1201
1386
|
p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
|
|
1202
1387
|
return;
|
|
1203
1388
|
}
|
|
1389
|
+
const changeLines = [];
|
|
1390
|
+
for (const m of toAdd) {
|
|
1391
|
+
const def = getModule(m);
|
|
1392
|
+
changeLines.push(`Adding ${def?.label ?? m}: appends a section to README.md, adds a link to CLAUDE.md`);
|
|
1393
|
+
}
|
|
1394
|
+
for (const m of toRemove) {
|
|
1395
|
+
const def = getModule(m);
|
|
1396
|
+
changeLines.push(`Removing ${def?.label ?? m}: removes its section from README.md and its link from CLAUDE.md`);
|
|
1397
|
+
}
|
|
1398
|
+
p.note(changeLines.join("\n"), label("Planned changes"));
|
|
1204
1399
|
let liProfileUrl;
|
|
1205
1400
|
if (toAdd.includes("linkedin") && !profile.linkedin?.profile_url) {
|
|
1206
1401
|
const url = await p.text({
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -69,3 +69,9 @@ Skip any area with nothing stale. If everything is fresh, say so in one line. Ke
|
|
|
69
69
|
|---------|-------------|
|
|
70
70
|
| `/add <description>` | Add a skill, project, or experience to your graph |
|
|
71
71
|
| `/reflect [slug]` | Review your graph for gaps, completions, and stale skills — also runs all installed module hooks |
|
|
72
|
+
|
|
73
|
+
## Modules
|
|
74
|
+
|
|
75
|
+
<!-- patina:modules:start -->
|
|
76
|
+
{{MODULES_SECTION}}
|
|
77
|
+
<!-- patina:modules:end -->
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# {{PATINA_NAME}}
|
|
2
|
+
|
|
3
|
+
<!-- patina:base:start -->
|
|
4
|
+
This is your patina — a personal knowledge base for your professional story.
|
|
5
|
+
|
|
6
|
+
- Profile and config live in `profile.yaml` and `CLAUDE.md`.
|
|
7
|
+
- Your notes, skills, and posts live in `{{CONTENT_DIR}}/`.
|
|
8
|
+
|
|
9
|
+
## Installed modules
|
|
10
|
+
|
|
11
|
+
Module-specific sections are appended below as you install modules.
|
|
12
|
+
<!-- patina:base:end -->
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# LinkedIn Module Context
|
|
2
|
+
|
|
3
|
+
This module helps you draft and refine your LinkedIn profile using your patina graph as the source of truth.
|
|
4
|
+
|
|
5
|
+
## Folder structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{CONTENT_DIR}}/linkedin/
|
|
9
|
+
INSTRUCTIONS.md — module-specific rules and guidance
|
|
10
|
+
LinkedIn Current State.md — your current live profile copy
|
|
11
|
+
LinkedIn About.md — draft for the About section
|
|
12
|
+
LinkedIn Headline.md — draft for your headline
|
|
13
|
+
LinkedIn Experience.md — draft for your experience entries
|
|
14
|
+
LinkedIn Skills.md — draft for your skills section
|
|
15
|
+
LinkedIn Featured.md — draft for featured content
|
|
16
|
+
LinkedIn Activity.md — draft for activity/posts section
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Slash commands
|
|
20
|
+
|
|
21
|
+
| Command | What it does |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| `/li-all` | Run all LinkedIn section drafts in sequence |
|
|
24
|
+
| `/li-about` | Draft or refine your LinkedIn About section |
|
|
25
|
+
| `/li-headline` | Draft or refine your LinkedIn headline |
|
|
26
|
+
| `/li-experience` | Draft or refine your LinkedIn experience entries |
|
|
27
|
+
| `/li-skills` | Draft or refine your LinkedIn skills section |
|
|
28
|
+
| `/li-featured` | Draft or refine your LinkedIn featured content |
|
|
29
|
+
| `/li-activity` | Draft or refine your LinkedIn activity section |
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
LinkedIn commands read your `{{CONTENT_DIR}}/` graph — notes, skills, and posts — and draft profile copy grounded in that evidence. They never invent claims not supported by your notes.
|
|
34
|
+
|
|
35
|
+
The `/reflect` command also runs the LinkedIn reflect hook (`/li-all`) to keep your drafts current.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Resume Module Context
|
|
2
|
+
|
|
3
|
+
This module helps you keep your resume current by synthesising it from your patina graph.
|
|
4
|
+
|
|
5
|
+
## Folder structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{CONTENT_DIR}}/resume/
|
|
9
|
+
INSTRUCTIONS.md — module-specific rules and guidance
|
|
10
|
+
Resume Working Draft.md — the resume you are actively editing
|
|
11
|
+
Resume Last Submitted.md — the version you last sent to an employer
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Slash commands
|
|
15
|
+
|
|
16
|
+
| Command | What it does |
|
|
17
|
+
|---------|-------------|
|
|
18
|
+
| `/resume-refresh` | Refresh your resume working draft from your graph |
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The `/resume-refresh` command reads your `{{CONTENT_DIR}}/` graph — notes, skills, and experience — and updates your Resume Working Draft to reflect your current professional state. It never overwrites Resume Last Submitted; that file is yours to update manually when you send an application.
|
|
23
|
+
|
|
24
|
+
The working draft is compared against the last submitted version so you can see what has changed before sending.
|