my-patina 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +278 -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);
|
|
@@ -242,6 +398,23 @@ function touch(targetDir, relativePath) {
|
|
|
242
398
|
mkdirSync2(dirname3(full), { recursive: true });
|
|
243
399
|
writeFileSync3(full, "", "utf8");
|
|
244
400
|
}
|
|
401
|
+
var MANIFEST_REQUIRED_FIELDS = ["name", "label", "reflect_hook", "description", "installed"];
|
|
402
|
+
function extractFrontmatter(content) {
|
|
403
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
404
|
+
if (!match) return null;
|
|
405
|
+
const parsed = yaml2.load(match[1]);
|
|
406
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
407
|
+
return parsed;
|
|
408
|
+
}
|
|
409
|
+
function validateManifestFrontmatter(moduleName, content) {
|
|
410
|
+
const fm = extractFrontmatter(content);
|
|
411
|
+
if (!fm) throw new Error(`Module "${moduleName}" manifest has missing or unparseable frontmatter`);
|
|
412
|
+
for (const field of MANIFEST_REQUIRED_FIELDS) {
|
|
413
|
+
if (!fm[field]) {
|
|
414
|
+
throw new Error(`Module "${moduleName}" manifest is missing required field "${field}"`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
245
418
|
function profileToVars(profile, liProfileUrl) {
|
|
246
419
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
247
420
|
return {
|
|
@@ -329,9 +502,16 @@ async function scaffold(opts) {
|
|
|
329
502
|
...baseManagedFiles(vars, editor, targetDir),
|
|
330
503
|
...modules.flatMap((m) => moduleManagedFiles(m, vars))
|
|
331
504
|
];
|
|
505
|
+
for (const module of modules) {
|
|
506
|
+
const manifestEntry = managedFiles.find(([p2]) => p2 === `.claude/modules/${module}/manifest.md`);
|
|
507
|
+
if (manifestEntry) validateManifestFrontmatter(module, manifestEntry[1]);
|
|
508
|
+
}
|
|
332
509
|
for (const [relativePath, content] of managedFiles) {
|
|
333
|
-
const
|
|
334
|
-
checksums[relativePath] = checksum;
|
|
510
|
+
const result = writeManagedFile(targetDir, relativePath, content, {});
|
|
511
|
+
checksums[relativePath] = result.checksum;
|
|
512
|
+
for (const s of result.sections ?? []) {
|
|
513
|
+
checksums[`${relativePath}:${s.id}`] = s.newChecksum;
|
|
514
|
+
}
|
|
335
515
|
}
|
|
336
516
|
const baseDirs = ["notes", "skills", "posts"];
|
|
337
517
|
for (const dir of baseDirs) {
|
|
@@ -353,7 +533,7 @@ ${STATE_FILENAME}
|
|
|
353
533
|
}
|
|
354
534
|
|
|
355
535
|
// src/validate.ts
|
|
356
|
-
import { existsSync as existsSync5, readFileSync as
|
|
536
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync } from "fs";
|
|
357
537
|
import { join as join6, relative, sep, basename } from "path";
|
|
358
538
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
359
539
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
@@ -425,7 +605,7 @@ function checkSkillNotes(root, profile) {
|
|
|
425
605
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
426
606
|
const issues = [];
|
|
427
607
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
428
|
-
const content =
|
|
608
|
+
const content = readFileSync6(skillFile, "utf8");
|
|
429
609
|
const links = extractWikiLinks(content);
|
|
430
610
|
for (const { target, line } of links) {
|
|
431
611
|
if (!noteSlugs.has(target)) {
|
|
@@ -452,7 +632,7 @@ function checkWikiLinks(root, profile) {
|
|
|
452
632
|
...listMarkdownFiles(postsDir)
|
|
453
633
|
];
|
|
454
634
|
for (const file of filesToScan) {
|
|
455
|
-
const content =
|
|
635
|
+
const content = readFileSync6(file, "utf8");
|
|
456
636
|
const links = extractWikiLinks(content);
|
|
457
637
|
for (const { target, line } of links) {
|
|
458
638
|
if (!noteSlugs.has(target)) {
|
|
@@ -472,7 +652,7 @@ function checkExclusions(root, profile) {
|
|
|
472
652
|
const notesDir = join6(contentDir, NOTES);
|
|
473
653
|
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
474
654
|
if (!existsSync5(exclusionsPath)) return [];
|
|
475
|
-
const content =
|
|
655
|
+
const content = readFileSync6(exclusionsPath, "utf8");
|
|
476
656
|
const items = parseExclusions(content);
|
|
477
657
|
if (items.length === 0) return [];
|
|
478
658
|
const skillsDir = join6(contentDir, SKILLS);
|
|
@@ -484,7 +664,7 @@ function checkExclusions(root, profile) {
|
|
|
484
664
|
const issues = [];
|
|
485
665
|
const seen = /* @__PURE__ */ new Set();
|
|
486
666
|
for (const file of filesToScan) {
|
|
487
|
-
const fileContent =
|
|
667
|
+
const fileContent = readFileSync6(file, "utf8");
|
|
488
668
|
const lines = fileContent.split("\n");
|
|
489
669
|
for (let i = 0; i < lines.length; i++) {
|
|
490
670
|
const lineText = lines[i];
|
|
@@ -721,6 +901,15 @@ function writeProfile(cwd, profile) {
|
|
|
721
901
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
722
902
|
const fullPath = join7(targetDir, rel);
|
|
723
903
|
if (!existsSync6(fullPath)) return "deleted";
|
|
904
|
+
const fileContent = readFileSync7(fullPath, "utf8");
|
|
905
|
+
if (hasFences(fileContent)) {
|
|
906
|
+
const editedIds = inspectSections(rel, fileContent, stored);
|
|
907
|
+
if (editedIds.length > 0) {
|
|
908
|
+
return "kept";
|
|
909
|
+
}
|
|
910
|
+
unlinkSync(fullPath);
|
|
911
|
+
return "deleted";
|
|
912
|
+
}
|
|
724
913
|
const currentHash = hashFile(fullPath);
|
|
725
914
|
const storedHash = stored[rel];
|
|
726
915
|
if (storedHash && currentHash !== storedHash) {
|
|
@@ -762,7 +951,7 @@ async function runUpdate(cwd) {
|
|
|
762
951
|
await runValidate(cwd, profile);
|
|
763
952
|
}
|
|
764
953
|
}
|
|
765
|
-
function applyProfileUpdate(cwd, profile, fields) {
|
|
954
|
+
function applyProfileUpdate(cwd, profile, fields, overwrite) {
|
|
766
955
|
const updatedProfile = {
|
|
767
956
|
...profile,
|
|
768
957
|
name: fields.name.trim(),
|
|
@@ -785,10 +974,19 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
785
974
|
];
|
|
786
975
|
const updated = [];
|
|
787
976
|
const skipped = [];
|
|
977
|
+
const keptSections = [];
|
|
788
978
|
for (const [rel, content] of files) {
|
|
789
|
-
const
|
|
790
|
-
newChecksums[rel] = checksum;
|
|
791
|
-
|
|
979
|
+
const result = writeManagedFile(cwd, rel, content, stored, overwrite);
|
|
980
|
+
newChecksums[rel] = result.checksum;
|
|
981
|
+
for (const s of result.sections ?? []) {
|
|
982
|
+
const sKey = `${rel}:${s.id}`;
|
|
983
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
984
|
+
else {
|
|
985
|
+
newChecksums[sKey] = stored[sKey] ?? "";
|
|
986
|
+
keptSections.push(sKey);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (result.outcome === "skipped") {
|
|
792
990
|
skipped.push(rel);
|
|
793
991
|
} else {
|
|
794
992
|
updated.push(rel);
|
|
@@ -802,7 +1000,7 @@ function applyProfileUpdate(cwd, profile, fields) {
|
|
|
802
1000
|
writeState(cwd, { checksums: newChecksums });
|
|
803
1001
|
const profileToWrite = stripLegacyChecksums(updatedProfile);
|
|
804
1002
|
writeProfile(cwd, profileToWrite);
|
|
805
|
-
return { profile: profileToWrite, updated, skipped };
|
|
1003
|
+
return { profile: profileToWrite, updated, skipped, keptSections };
|
|
806
1004
|
}
|
|
807
1005
|
async function runUpdateProfile(cwd, profile) {
|
|
808
1006
|
console.log("");
|
|
@@ -855,7 +1053,7 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
855
1053
|
},
|
|
856
1054
|
{ onCancel }
|
|
857
1055
|
);
|
|
858
|
-
const
|
|
1056
|
+
const fields = {
|
|
859
1057
|
name: identity.name,
|
|
860
1058
|
title: identity.title ?? "",
|
|
861
1059
|
roleDescription: identity.roleDescription ?? "",
|
|
@@ -864,11 +1062,52 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
864
1062
|
companyName: work.companyName ?? "",
|
|
865
1063
|
website: work.website ?? "",
|
|
866
1064
|
companyDescription: work.companyDescription ?? ""
|
|
867
|
-
}
|
|
1065
|
+
};
|
|
1066
|
+
const overwriteSet = /* @__PURE__ */ new Set();
|
|
1067
|
+
const previewProfile = {
|
|
1068
|
+
...profile,
|
|
1069
|
+
name: fields.name.trim(),
|
|
1070
|
+
title: fields.title.trim(),
|
|
1071
|
+
role_description: fields.roleDescription.trim() || void 0,
|
|
1072
|
+
job_description_url: fields.jobDescriptionUrl.trim() || void 0,
|
|
1073
|
+
work: {
|
|
1074
|
+
self_employed: fields.selfEmployed,
|
|
1075
|
+
company_name: fields.companyName.trim() || (fields.selfEmployed ? "Freelance" : ""),
|
|
1076
|
+
website: fields.website.trim() || void 0,
|
|
1077
|
+
company_description: fields.companyDescription.trim() || void 0
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
const previewVars = profileToVars(previewProfile);
|
|
1081
|
+
const storedChecksums = readState(cwd, profile).checksums;
|
|
1082
|
+
const previewFiles = [
|
|
1083
|
+
...baseManagedFiles(previewVars, previewProfile.editor, cwd),
|
|
1084
|
+
...previewProfile.modules.flatMap((m) => moduleManagedFiles(m, previewVars))
|
|
1085
|
+
];
|
|
1086
|
+
for (const [rel, content] of previewFiles) {
|
|
1087
|
+
if (hasFences(content)) {
|
|
1088
|
+
const fullPath = join7(cwd, rel);
|
|
1089
|
+
if (existsSync6(fullPath)) {
|
|
1090
|
+
const existingContent = readFileSync7(fullPath, "utf8");
|
|
1091
|
+
const editedIds = inspectSections(rel, existingContent, storedChecksums);
|
|
1092
|
+
for (const sectionId of editedIds) {
|
|
1093
|
+
const confirmed = await p.confirm({
|
|
1094
|
+
message: `Section '${sectionId}' in ${rel} has been manually edited. Overwrite?`,
|
|
1095
|
+
initialValue: false
|
|
1096
|
+
});
|
|
1097
|
+
if (p.isCancel(confirmed)) onCancel();
|
|
1098
|
+
if (confirmed) overwriteSet.add(sectionId);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const { updated, skipped, keptSections } = applyProfileUpdate(cwd, profile, fields, overwriteSet);
|
|
868
1104
|
const summaryLines = [];
|
|
869
1105
|
if (updated.length > 0) {
|
|
870
1106
|
summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
|
|
871
1107
|
}
|
|
1108
|
+
if (keptSections.length > 0) {
|
|
1109
|
+
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSections.join(", ")}`));
|
|
1110
|
+
}
|
|
872
1111
|
if (skipped.length > 0) {
|
|
873
1112
|
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skipped.join(", ")}`));
|
|
874
1113
|
}
|
|
@@ -883,6 +1122,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
883
1122
|
const skippedFiles = [];
|
|
884
1123
|
const deleted = [];
|
|
885
1124
|
const kept = [];
|
|
1125
|
+
const keptSections = [];
|
|
886
1126
|
for (const module of toAdd) {
|
|
887
1127
|
const def = getModule(module);
|
|
888
1128
|
if (def?.onAdd) {
|
|
@@ -891,9 +1131,17 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
891
1131
|
const vars = profileToVars(updatedProfile);
|
|
892
1132
|
const contentDir = updatedProfile.content_dir;
|
|
893
1133
|
for (const [rel, content] of moduleManagedFiles(module, vars)) {
|
|
894
|
-
const
|
|
895
|
-
newChecksums[rel] = checksum;
|
|
896
|
-
|
|
1134
|
+
const result = writeManagedFile(cwd, rel, content, newChecksums);
|
|
1135
|
+
newChecksums[rel] = result.checksum;
|
|
1136
|
+
for (const s of result.sections ?? []) {
|
|
1137
|
+
const sKey = `${rel}:${s.id}`;
|
|
1138
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1139
|
+
else {
|
|
1140
|
+
newChecksums[sKey] = newChecksums[sKey] ?? "";
|
|
1141
|
+
keptSections.push(sKey);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (result.outcome === "skipped") {
|
|
897
1145
|
skippedFiles.push(rel);
|
|
898
1146
|
} else {
|
|
899
1147
|
added.push(rel);
|
|
@@ -919,6 +1167,10 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
919
1167
|
if (result === "deleted") {
|
|
920
1168
|
deleted.push(rel);
|
|
921
1169
|
delete newChecksums[rel];
|
|
1170
|
+
const prefix = rel + ":";
|
|
1171
|
+
for (const key of Object.keys(newChecksums)) {
|
|
1172
|
+
if (key.startsWith(prefix)) delete newChecksums[key];
|
|
1173
|
+
}
|
|
922
1174
|
} else {
|
|
923
1175
|
kept.push(rel);
|
|
924
1176
|
}
|
|
@@ -931,7 +1183,7 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
931
1183
|
writeState(cwd, { checksums: newChecksums });
|
|
932
1184
|
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
933
1185
|
writeProfile(cwd, finalProfile);
|
|
934
|
-
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept };
|
|
1186
|
+
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept, keptSections };
|
|
935
1187
|
}
|
|
936
1188
|
async function runUpdateModules(cwd, profile) {
|
|
937
1189
|
const currentModules = profile.modules ?? [];
|
|
@@ -970,9 +1222,10 @@ async function runUpdateModules(cwd, profile) {
|
|
|
970
1222
|
liProfileUrl = url.trim();
|
|
971
1223
|
}
|
|
972
1224
|
}
|
|
973
|
-
const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
|
|
1225
|
+
const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles, keptSections: keptSectionKeys } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
|
|
974
1226
|
const summaryLines = [];
|
|
975
1227
|
if (addedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Added: ${addedFiles.join(", ")}`));
|
|
1228
|
+
if (keptSectionKeys.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSectionKeys.join(", ")}`));
|
|
976
1229
|
if (skippedFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skippedFiles.join(", ")}`));
|
|
977
1230
|
if (deletedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Removed: ${deletedFiles.join(", ")}`));
|
|
978
1231
|
if (keptFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edited files: ${keptFiles.join(", ")}`));
|
|
@@ -1001,7 +1254,7 @@ function registerCommands(_program) {
|
|
|
1001
1254
|
// src/cli.ts
|
|
1002
1255
|
import chalk2 from "chalk";
|
|
1003
1256
|
var program = new Command();
|
|
1004
|
-
program.name("patina").description("Personal professional knowledge graph \u2014 setup and management");
|
|
1257
|
+
program.name("patina").description("Personal professional knowledge graph \u2014 setup and management").allowExcessArguments(true);
|
|
1005
1258
|
program.command("validate").description("Check your patina for broken links and excluded items").action(() => {
|
|
1006
1259
|
try {
|
|
1007
1260
|
const cwd = process.cwd();
|
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": [
|