my-patina 0.5.0 → 0.7.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 +263 -38
- package/dist/templates/CLAUDE.md +21 -0
- package/package.json +4 -4
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
|
|
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 =
|
|
354
|
+
const raw = readFileSync5(statePath, "utf8");
|
|
199
355
|
let parsed;
|
|
200
356
|
try {
|
|
201
357
|
parsed = JSON.parse(raw);
|
|
@@ -271,7 +427,11 @@ function profileToVars(profile, liProfileUrl) {
|
|
|
271
427
|
CONTENT_DIR: profile.content_dir,
|
|
272
428
|
EDITOR: profile.editor,
|
|
273
429
|
LI_PROFILE_URL: liProfileUrl ?? profile.linkedin?.profile_url ?? "",
|
|
274
|
-
TODAY: today
|
|
430
|
+
TODAY: today,
|
|
431
|
+
STALENESS_THRESHOLD: (() => {
|
|
432
|
+
const d = Number(profile.staleness_threshold_days ?? 30);
|
|
433
|
+
return String(Number.isFinite(d) && d > 0 ? d : 30);
|
|
434
|
+
})()
|
|
275
435
|
};
|
|
276
436
|
}
|
|
277
437
|
function baseManagedFiles(vars, editor, targetDir) {
|
|
@@ -328,18 +488,7 @@ async function scaffold(opts) {
|
|
|
328
488
|
created: today,
|
|
329
489
|
...modules.includes("linkedin") && liProfileUrl ? { linkedin: { profile_url: liProfileUrl } } : {}
|
|
330
490
|
};
|
|
331
|
-
const vars =
|
|
332
|
-
PATINA_NAME: patinaName,
|
|
333
|
-
USER_NAME: userName,
|
|
334
|
-
USER_TITLE: title ?? "",
|
|
335
|
-
ROLE_DESCRIPTION: roleDescription,
|
|
336
|
-
COMPANY_NAME: work.company_name,
|
|
337
|
-
COMPANY_DESCRIPTION: work.company_description ?? "",
|
|
338
|
-
CONTENT_DIR: contentDir,
|
|
339
|
-
EDITOR: editor,
|
|
340
|
-
LI_PROFILE_URL: liProfileUrl,
|
|
341
|
-
TODAY: today
|
|
342
|
-
};
|
|
491
|
+
const vars = profileToVars(tempProfile, liProfileUrl);
|
|
343
492
|
mkdirSync2(targetDir, { recursive: true });
|
|
344
493
|
const checksums = {};
|
|
345
494
|
const managedFiles = [
|
|
@@ -351,8 +500,11 @@ async function scaffold(opts) {
|
|
|
351
500
|
if (manifestEntry) validateManifestFrontmatter(module, manifestEntry[1]);
|
|
352
501
|
}
|
|
353
502
|
for (const [relativePath, content] of managedFiles) {
|
|
354
|
-
const
|
|
355
|
-
checksums[relativePath] = checksum;
|
|
503
|
+
const result = writeManagedFile(targetDir, relativePath, content, {});
|
|
504
|
+
checksums[relativePath] = result.checksum;
|
|
505
|
+
for (const s of result.sections ?? []) {
|
|
506
|
+
checksums[`${relativePath}:${s.id}`] = s.newChecksum;
|
|
507
|
+
}
|
|
356
508
|
}
|
|
357
509
|
const baseDirs = ["notes", "skills", "posts"];
|
|
358
510
|
for (const dir of baseDirs) {
|
|
@@ -374,7 +526,7 @@ ${STATE_FILENAME}
|
|
|
374
526
|
}
|
|
375
527
|
|
|
376
528
|
// src/validate.ts
|
|
377
|
-
import { existsSync as existsSync5, readFileSync as
|
|
529
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync } from "fs";
|
|
378
530
|
import { join as join6, relative, sep, basename } from "path";
|
|
379
531
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
380
532
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
@@ -446,7 +598,7 @@ function checkSkillNotes(root, profile) {
|
|
|
446
598
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
447
599
|
const issues = [];
|
|
448
600
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
449
|
-
const content =
|
|
601
|
+
const content = readFileSync6(skillFile, "utf8");
|
|
450
602
|
const links = extractWikiLinks(content);
|
|
451
603
|
for (const { target, line } of links) {
|
|
452
604
|
if (!noteSlugs.has(target)) {
|
|
@@ -473,7 +625,7 @@ function checkWikiLinks(root, profile) {
|
|
|
473
625
|
...listMarkdownFiles(postsDir)
|
|
474
626
|
];
|
|
475
627
|
for (const file of filesToScan) {
|
|
476
|
-
const content =
|
|
628
|
+
const content = readFileSync6(file, "utf8");
|
|
477
629
|
const links = extractWikiLinks(content);
|
|
478
630
|
for (const { target, line } of links) {
|
|
479
631
|
if (!noteSlugs.has(target)) {
|
|
@@ -493,7 +645,7 @@ function checkExclusions(root, profile) {
|
|
|
493
645
|
const notesDir = join6(contentDir, NOTES);
|
|
494
646
|
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
495
647
|
if (!existsSync5(exclusionsPath)) return [];
|
|
496
|
-
const content =
|
|
648
|
+
const content = readFileSync6(exclusionsPath, "utf8");
|
|
497
649
|
const items = parseExclusions(content);
|
|
498
650
|
if (items.length === 0) return [];
|
|
499
651
|
const skillsDir = join6(contentDir, SKILLS);
|
|
@@ -505,7 +657,7 @@ function checkExclusions(root, profile) {
|
|
|
505
657
|
const issues = [];
|
|
506
658
|
const seen = /* @__PURE__ */ new Set();
|
|
507
659
|
for (const file of filesToScan) {
|
|
508
|
-
const fileContent =
|
|
660
|
+
const fileContent = readFileSync6(file, "utf8");
|
|
509
661
|
const lines = fileContent.split("\n");
|
|
510
662
|
for (let i = 0; i < lines.length; i++) {
|
|
511
663
|
const lineText = lines[i];
|
|
@@ -742,6 +894,15 @@ function writeProfile(cwd, profile) {
|
|
|
742
894
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
743
895
|
const fullPath = join7(targetDir, rel);
|
|
744
896
|
if (!existsSync6(fullPath)) return "deleted";
|
|
897
|
+
const fileContent = readFileSync7(fullPath, "utf8");
|
|
898
|
+
if (hasFences(fileContent)) {
|
|
899
|
+
const editedIds = inspectSections(rel, fileContent, stored);
|
|
900
|
+
if (editedIds.length > 0) {
|
|
901
|
+
return "kept";
|
|
902
|
+
}
|
|
903
|
+
unlinkSync(fullPath);
|
|
904
|
+
return "deleted";
|
|
905
|
+
}
|
|
745
906
|
const currentHash = hashFile(fullPath);
|
|
746
907
|
const storedHash = stored[rel];
|
|
747
908
|
if (storedHash && currentHash !== storedHash) {
|
|
@@ -783,7 +944,7 @@ async function runUpdate(cwd) {
|
|
|
783
944
|
await runValidate(cwd, profile);
|
|
784
945
|
}
|
|
785
946
|
}
|
|
786
|
-
function applyProfileUpdate(cwd, profile, fields) {
|
|
947
|
+
function applyProfileUpdate(cwd, profile, fields, overwrite) {
|
|
787
948
|
const updatedProfile = {
|
|
788
949
|
...profile,
|
|
789
950
|
name: fields.name.trim(),
|
|
@@ -806,10 +967,19 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
806
967
|
];
|
|
807
968
|
const updated = [];
|
|
808
969
|
const skipped = [];
|
|
970
|
+
const keptSections = [];
|
|
809
971
|
for (const [rel, content] of files) {
|
|
810
|
-
const
|
|
811
|
-
newChecksums[rel] = checksum;
|
|
812
|
-
|
|
972
|
+
const result = writeManagedFile(cwd, rel, content, stored, overwrite);
|
|
973
|
+
newChecksums[rel] = result.checksum;
|
|
974
|
+
for (const s of result.sections ?? []) {
|
|
975
|
+
const sKey = `${rel}:${s.id}`;
|
|
976
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
977
|
+
else {
|
|
978
|
+
newChecksums[sKey] = stored[sKey] ?? "";
|
|
979
|
+
keptSections.push(sKey);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (result.outcome === "skipped") {
|
|
813
983
|
skipped.push(rel);
|
|
814
984
|
} else {
|
|
815
985
|
updated.push(rel);
|
|
@@ -823,7 +993,7 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
823
993
|
writeState(cwd, { checksums: newChecksums });
|
|
824
994
|
const profileToWrite = stripLegacyChecksums(updatedProfile);
|
|
825
995
|
writeProfile(cwd, profileToWrite);
|
|
826
|
-
return { profile: profileToWrite, updated, skipped };
|
|
996
|
+
return { profile: profileToWrite, updated, skipped, keptSections };
|
|
827
997
|
}
|
|
828
998
|
async function runUpdateProfile(cwd, profile) {
|
|
829
999
|
console.log("");
|
|
@@ -876,7 +1046,7 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
876
1046
|
},
|
|
877
1047
|
{ onCancel }
|
|
878
1048
|
);
|
|
879
|
-
const
|
|
1049
|
+
const fields = {
|
|
880
1050
|
name: identity.name,
|
|
881
1051
|
title: identity.title ?? "",
|
|
882
1052
|
roleDescription: identity.roleDescription ?? "",
|
|
@@ -885,11 +1055,52 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
885
1055
|
companyName: work.companyName ?? "",
|
|
886
1056
|
website: work.website ?? "",
|
|
887
1057
|
companyDescription: work.companyDescription ?? ""
|
|
888
|
-
}
|
|
1058
|
+
};
|
|
1059
|
+
const overwriteSet = /* @__PURE__ */ new Set();
|
|
1060
|
+
const previewProfile = {
|
|
1061
|
+
...profile,
|
|
1062
|
+
name: fields.name.trim(),
|
|
1063
|
+
title: fields.title.trim(),
|
|
1064
|
+
role_description: fields.roleDescription.trim() || void 0,
|
|
1065
|
+
job_description_url: fields.jobDescriptionUrl.trim() || void 0,
|
|
1066
|
+
work: {
|
|
1067
|
+
self_employed: fields.selfEmployed,
|
|
1068
|
+
company_name: fields.companyName.trim() || (fields.selfEmployed ? "Freelance" : ""),
|
|
1069
|
+
website: fields.website.trim() || void 0,
|
|
1070
|
+
company_description: fields.companyDescription.trim() || void 0
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
const previewVars = profileToVars(previewProfile);
|
|
1074
|
+
const storedChecksums = readState(cwd, profile).checksums;
|
|
1075
|
+
const previewFiles = [
|
|
1076
|
+
...baseManagedFiles(previewVars, previewProfile.editor, cwd),
|
|
1077
|
+
...previewProfile.modules.flatMap((m) => moduleManagedFiles(m, previewVars))
|
|
1078
|
+
];
|
|
1079
|
+
for (const [rel, content] of previewFiles) {
|
|
1080
|
+
if (hasFences(content)) {
|
|
1081
|
+
const fullPath = join7(cwd, rel);
|
|
1082
|
+
if (existsSync6(fullPath)) {
|
|
1083
|
+
const existingContent = readFileSync7(fullPath, "utf8");
|
|
1084
|
+
const editedIds = inspectSections(rel, existingContent, storedChecksums);
|
|
1085
|
+
for (const sectionId of editedIds) {
|
|
1086
|
+
const confirmed = await p.confirm({
|
|
1087
|
+
message: `Section '${sectionId}' in ${rel} has been manually edited. Overwrite?`,
|
|
1088
|
+
initialValue: false
|
|
1089
|
+
});
|
|
1090
|
+
if (p.isCancel(confirmed)) onCancel();
|
|
1091
|
+
if (confirmed) overwriteSet.add(sectionId);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const { updated, skipped, keptSections } = applyProfileUpdate(cwd, profile, fields, overwriteSet);
|
|
889
1097
|
const summaryLines = [];
|
|
890
1098
|
if (updated.length > 0) {
|
|
891
1099
|
summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
|
|
892
1100
|
}
|
|
1101
|
+
if (keptSections.length > 0) {
|
|
1102
|
+
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSections.join(", ")}`));
|
|
1103
|
+
}
|
|
893
1104
|
if (skipped.length > 0) {
|
|
894
1105
|
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skipped.join(", ")}`));
|
|
895
1106
|
}
|
|
@@ -904,6 +1115,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
904
1115
|
const skippedFiles = [];
|
|
905
1116
|
const deleted = [];
|
|
906
1117
|
const kept = [];
|
|
1118
|
+
const keptSections = [];
|
|
907
1119
|
for (const module of toAdd) {
|
|
908
1120
|
const def = getModule(module);
|
|
909
1121
|
if (def?.onAdd) {
|
|
@@ -912,9 +1124,17 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
912
1124
|
const vars = profileToVars(updatedProfile);
|
|
913
1125
|
const contentDir = updatedProfile.content_dir;
|
|
914
1126
|
for (const [rel, content] of moduleManagedFiles(module, vars)) {
|
|
915
|
-
const
|
|
916
|
-
newChecksums[rel] = checksum;
|
|
917
|
-
|
|
1127
|
+
const result = writeManagedFile(cwd, rel, content, newChecksums);
|
|
1128
|
+
newChecksums[rel] = result.checksum;
|
|
1129
|
+
for (const s of result.sections ?? []) {
|
|
1130
|
+
const sKey = `${rel}:${s.id}`;
|
|
1131
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1132
|
+
else {
|
|
1133
|
+
newChecksums[sKey] = newChecksums[sKey] ?? "";
|
|
1134
|
+
keptSections.push(sKey);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (result.outcome === "skipped") {
|
|
918
1138
|
skippedFiles.push(rel);
|
|
919
1139
|
} else {
|
|
920
1140
|
added.push(rel);
|
|
@@ -940,6 +1160,10 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
940
1160
|
if (result === "deleted") {
|
|
941
1161
|
deleted.push(rel);
|
|
942
1162
|
delete newChecksums[rel];
|
|
1163
|
+
const prefix = rel + ":";
|
|
1164
|
+
for (const key of Object.keys(newChecksums)) {
|
|
1165
|
+
if (key.startsWith(prefix)) delete newChecksums[key];
|
|
1166
|
+
}
|
|
943
1167
|
} else {
|
|
944
1168
|
kept.push(rel);
|
|
945
1169
|
}
|
|
@@ -952,7 +1176,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
952
1176
|
writeState(cwd, { checksums: newChecksums });
|
|
953
1177
|
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
954
1178
|
writeProfile(cwd, finalProfile);
|
|
955
|
-
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept };
|
|
1179
|
+
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept, keptSections };
|
|
956
1180
|
}
|
|
957
1181
|
async function runUpdateModules(cwd, profile) {
|
|
958
1182
|
const currentModules = profile.modules ?? [];
|
|
@@ -991,9 +1215,10 @@ async function runUpdateModules(cwd, profile) {
|
|
|
991
1215
|
liProfileUrl = url.trim();
|
|
992
1216
|
}
|
|
993
1217
|
}
|
|
994
|
-
const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
|
|
1218
|
+
const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles, keptSections: keptSectionKeys } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
|
|
995
1219
|
const summaryLines = [];
|
|
996
1220
|
if (addedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Added: ${addedFiles.join(", ")}`));
|
|
1221
|
+
if (keptSectionKeys.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSectionKeys.join(", ")}`));
|
|
997
1222
|
if (skippedFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skippedFiles.join(", ")}`));
|
|
998
1223
|
if (deletedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Removed: ${deletedFiles.join(", ")}`));
|
|
999
1224
|
if (keptFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edited files: ${keptFiles.join(", ")}`));
|
|
@@ -1022,7 +1247,7 @@ function registerCommands(_program) {
|
|
|
1022
1247
|
// src/cli.ts
|
|
1023
1248
|
import chalk2 from "chalk";
|
|
1024
1249
|
var program = new Command();
|
|
1025
|
-
program.name("patina").description("Personal professional knowledge graph \u2014 setup and management");
|
|
1250
|
+
program.name("patina").description("Personal professional knowledge graph \u2014 setup and management").allowExcessArguments(true);
|
|
1026
1251
|
program.command("validate").description("Check your patina for broken links and excluded items").action(() => {
|
|
1027
1252
|
try {
|
|
1028
1253
|
const cwd = process.cwd();
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
|
@@ -42,6 +44,25 @@ The graph is the source of truth. Nothing gets added to generated content unless
|
|
|
42
44
|
- Never delete skill files automatically. Surface them to the user and wait for confirmation.
|
|
43
45
|
- `{{CONTENT_DIR}}/notes/exclusions.md` overrides everything. If something is listed there, it must not appear in any generated output.
|
|
44
46
|
|
|
47
|
+
## On session start
|
|
48
|
+
|
|
49
|
+
Before anything else, scan the graph for stale content and surface a brief report.
|
|
50
|
+
|
|
51
|
+
Read the file modification times for all three areas — skip `.gitkeep`, `README.md`, and `exclusions.md` in every directory:
|
|
52
|
+
- `{{CONTENT_DIR}}/notes/`
|
|
53
|
+
- `{{CONTENT_DIR}}/skills/`
|
|
54
|
+
- `{{CONTENT_DIR}}/posts/`
|
|
55
|
+
|
|
56
|
+
List items not modified in the last **{{STALENESS_THRESHOLD}} days**, grouped by area:
|
|
57
|
+
|
|
58
|
+
- **Notes** — stale note slugs
|
|
59
|
+
- **Skills** — stale skill slugs
|
|
60
|
+
- **Posts** — stale draft slugs
|
|
61
|
+
|
|
62
|
+
Skip any area with nothing stale. If everything is fresh, say so in one line. Keep the report brief — one line per area. Then ask:
|
|
63
|
+
|
|
64
|
+
> What are we working on today?
|
|
65
|
+
|
|
45
66
|
## Slash commands
|
|
46
67
|
|
|
47
68
|
| Command | What it does |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-patina",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": "^
|
|
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": "^
|
|
32
|
-
"vitest": "^1.
|
|
31
|
+
"typescript": "^6.0.3",
|
|
32
|
+
"vitest": "^4.1.7"
|
|
33
33
|
},
|
|
34
34
|
"tsup": {
|
|
35
35
|
"entry": [
|