my-patina 0.5.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 +257 -25
- package/dist/templates/CLAUDE.md +2 -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);
|
|
@@ -351,8 +507,11 @@ async function scaffold(opts) {
|
|
|
351
507
|
if (manifestEntry) validateManifestFrontmatter(module, manifestEntry[1]);
|
|
352
508
|
}
|
|
353
509
|
for (const [relativePath, content] of managedFiles) {
|
|
354
|
-
const
|
|
355
|
-
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
|
+
}
|
|
356
515
|
}
|
|
357
516
|
const baseDirs = ["notes", "skills", "posts"];
|
|
358
517
|
for (const dir of baseDirs) {
|
|
@@ -374,7 +533,7 @@ ${STATE_FILENAME}
|
|
|
374
533
|
}
|
|
375
534
|
|
|
376
535
|
// src/validate.ts
|
|
377
|
-
import { existsSync as existsSync5, readFileSync as
|
|
536
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync } from "fs";
|
|
378
537
|
import { join as join6, relative, sep, basename } from "path";
|
|
379
538
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
380
539
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
@@ -446,7 +605,7 @@ function checkSkillNotes(root, profile) {
|
|
|
446
605
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
447
606
|
const issues = [];
|
|
448
607
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
449
|
-
const content =
|
|
608
|
+
const content = readFileSync6(skillFile, "utf8");
|
|
450
609
|
const links = extractWikiLinks(content);
|
|
451
610
|
for (const { target, line } of links) {
|
|
452
611
|
if (!noteSlugs.has(target)) {
|
|
@@ -473,7 +632,7 @@ function checkWikiLinks(root, profile) {
|
|
|
473
632
|
...listMarkdownFiles(postsDir)
|
|
474
633
|
];
|
|
475
634
|
for (const file of filesToScan) {
|
|
476
|
-
const content =
|
|
635
|
+
const content = readFileSync6(file, "utf8");
|
|
477
636
|
const links = extractWikiLinks(content);
|
|
478
637
|
for (const { target, line } of links) {
|
|
479
638
|
if (!noteSlugs.has(target)) {
|
|
@@ -493,7 +652,7 @@ function checkExclusions(root, profile) {
|
|
|
493
652
|
const notesDir = join6(contentDir, NOTES);
|
|
494
653
|
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
495
654
|
if (!existsSync5(exclusionsPath)) return [];
|
|
496
|
-
const content =
|
|
655
|
+
const content = readFileSync6(exclusionsPath, "utf8");
|
|
497
656
|
const items = parseExclusions(content);
|
|
498
657
|
if (items.length === 0) return [];
|
|
499
658
|
const skillsDir = join6(contentDir, SKILLS);
|
|
@@ -505,7 +664,7 @@ function checkExclusions(root, profile) {
|
|
|
505
664
|
const issues = [];
|
|
506
665
|
const seen = /* @__PURE__ */ new Set();
|
|
507
666
|
for (const file of filesToScan) {
|
|
508
|
-
const fileContent =
|
|
667
|
+
const fileContent = readFileSync6(file, "utf8");
|
|
509
668
|
const lines = fileContent.split("\n");
|
|
510
669
|
for (let i = 0; i < lines.length; i++) {
|
|
511
670
|
const lineText = lines[i];
|
|
@@ -742,6 +901,15 @@ function writeProfile(cwd, profile) {
|
|
|
742
901
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
743
902
|
const fullPath = join7(targetDir, rel);
|
|
744
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
|
+
}
|
|
745
913
|
const currentHash = hashFile(fullPath);
|
|
746
914
|
const storedHash = stored[rel];
|
|
747
915
|
if (storedHash && currentHash !== storedHash) {
|
|
@@ -783,7 +951,7 @@ async function runUpdate(cwd) {
|
|
|
783
951
|
await runValidate(cwd, profile);
|
|
784
952
|
}
|
|
785
953
|
}
|
|
786
|
-
function applyProfileUpdate(cwd, profile, fields) {
|
|
954
|
+
function applyProfileUpdate(cwd, profile, fields, overwrite) {
|
|
787
955
|
const updatedProfile = {
|
|
788
956
|
...profile,
|
|
789
957
|
name: fields.name.trim(),
|
|
@@ -806,10 +974,19 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
806
974
|
];
|
|
807
975
|
const updated = [];
|
|
808
976
|
const skipped = [];
|
|
977
|
+
const keptSections = [];
|
|
809
978
|
for (const [rel, content] of files) {
|
|
810
|
-
const
|
|
811
|
-
newChecksums[rel] = checksum;
|
|
812
|
-
|
|
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") {
|
|
813
990
|
skipped.push(rel);
|
|
814
991
|
} else {
|
|
815
992
|
updated.push(rel);
|
|
@@ -823,7 +1000,7 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
823
1000
|
writeState(cwd, { checksums: newChecksums });
|
|
824
1001
|
const profileToWrite = stripLegacyChecksums(updatedProfile);
|
|
825
1002
|
writeProfile(cwd, profileToWrite);
|
|
826
|
-
return { profile: profileToWrite, updated, skipped };
|
|
1003
|
+
return { profile: profileToWrite, updated, skipped, keptSections };
|
|
827
1004
|
}
|
|
828
1005
|
async function runUpdateProfile(cwd, profile) {
|
|
829
1006
|
console.log("");
|
|
@@ -876,7 +1053,7 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
876
1053
|
},
|
|
877
1054
|
{ onCancel }
|
|
878
1055
|
);
|
|
879
|
-
const
|
|
1056
|
+
const fields = {
|
|
880
1057
|
name: identity.name,
|
|
881
1058
|
title: identity.title ?? "",
|
|
882
1059
|
roleDescription: identity.roleDescription ?? "",
|
|
@@ -885,11 +1062,52 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
885
1062
|
companyName: work.companyName ?? "",
|
|
886
1063
|
website: work.website ?? "",
|
|
887
1064
|
companyDescription: work.companyDescription ?? ""
|
|
888
|
-
}
|
|
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);
|
|
889
1104
|
const summaryLines = [];
|
|
890
1105
|
if (updated.length > 0) {
|
|
891
1106
|
summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
|
|
892
1107
|
}
|
|
1108
|
+
if (keptSections.length > 0) {
|
|
1109
|
+
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSections.join(", ")}`));
|
|
1110
|
+
}
|
|
893
1111
|
if (skipped.length > 0) {
|
|
894
1112
|
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skipped.join(", ")}`));
|
|
895
1113
|
}
|
|
@@ -904,6 +1122,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
904
1122
|
const skippedFiles = [];
|
|
905
1123
|
const deleted = [];
|
|
906
1124
|
const kept = [];
|
|
1125
|
+
const keptSections = [];
|
|
907
1126
|
for (const module of toAdd) {
|
|
908
1127
|
const def = getModule(module);
|
|
909
1128
|
if (def?.onAdd) {
|
|
@@ -912,9 +1131,17 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
912
1131
|
const vars = profileToVars(updatedProfile);
|
|
913
1132
|
const contentDir = updatedProfile.content_dir;
|
|
914
1133
|
for (const [rel, content] of moduleManagedFiles(module, vars)) {
|
|
915
|
-
const
|
|
916
|
-
newChecksums[rel] = checksum;
|
|
917
|
-
|
|
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") {
|
|
918
1145
|
skippedFiles.push(rel);
|
|
919
1146
|
} else {
|
|
920
1147
|
added.push(rel);
|
|
@@ -940,6 +1167,10 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
940
1167
|
if (result === "deleted") {
|
|
941
1168
|
deleted.push(rel);
|
|
942
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
|
+
}
|
|
943
1174
|
} else {
|
|
944
1175
|
kept.push(rel);
|
|
945
1176
|
}
|
|
@@ -952,7 +1183,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
952
1183
|
writeState(cwd, { checksums: newChecksums });
|
|
953
1184
|
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
954
1185
|
writeProfile(cwd, finalProfile);
|
|
955
|
-
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept };
|
|
1186
|
+
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept, keptSections };
|
|
956
1187
|
}
|
|
957
1188
|
async function runUpdateModules(cwd, profile) {
|
|
958
1189
|
const currentModules = profile.modules ?? [];
|
|
@@ -991,9 +1222,10 @@ async function runUpdateModules(cwd, profile) {
|
|
|
991
1222
|
liProfileUrl = url.trim();
|
|
992
1223
|
}
|
|
993
1224
|
}
|
|
994
|
-
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 } });
|
|
995
1226
|
const summaryLines = [];
|
|
996
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(", ")}`));
|
|
997
1229
|
if (skippedFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skippedFiles.join(", ")}`));
|
|
998
1230
|
if (deletedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Removed: ${deletedFiles.join(", ")}`));
|
|
999
1231
|
if (keptFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edited files: ${keptFiles.join(", ")}`));
|
|
@@ -1022,7 +1254,7 @@ function registerCommands(_program) {
|
|
|
1022
1254
|
// src/cli.ts
|
|
1023
1255
|
import chalk2 from "chalk";
|
|
1024
1256
|
var program = new Command();
|
|
1025
|
-
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);
|
|
1026
1258
|
program.command("validate").description("Check your patina for broken links and excluded items").action(() => {
|
|
1027
1259
|
try {
|
|
1028
1260
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-patina",
|
|
3
|
-
"version": "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": "^
|
|
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": [
|