my-patina 0.1.1 → 0.2.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 +341 -250
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/wizard.ts
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import { dirname as
|
|
7
|
-
import { existsSync as
|
|
6
|
+
import { dirname as dirname4, join as join7, resolve } from "path";
|
|
7
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
8
8
|
import yaml3 from "js-yaml";
|
|
9
9
|
|
|
10
10
|
// src/detect.ts
|
|
@@ -20,9 +20,8 @@ function loadProfile(root) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// src/scaffold.ts
|
|
23
|
-
import { mkdirSync as mkdirSync2, writeFileSync as
|
|
24
|
-
import { join as
|
|
25
|
-
import { fileURLToPath } from "url";
|
|
23
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
24
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
26
25
|
import yaml2 from "js-yaml";
|
|
27
26
|
|
|
28
27
|
// src/template.ts
|
|
@@ -34,65 +33,139 @@ function render(template, vars) {
|
|
|
34
33
|
|
|
35
34
|
// src/upgrade.ts
|
|
36
35
|
import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
37
|
-
import { join as
|
|
36
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
38
37
|
|
|
39
38
|
// src/checksums.ts
|
|
40
39
|
import { createHash } from "crypto";
|
|
41
|
-
import { readFileSync as
|
|
40
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
41
|
+
|
|
42
|
+
// src/template-loader.ts
|
|
43
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
44
|
+
import { join as join2, dirname } from "path";
|
|
45
|
+
import { fileURLToPath } from "url";
|
|
46
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
var TEMPLATES_DIR = join2(__dirname, "templates");
|
|
48
|
+
function tpl(relativePath) {
|
|
49
|
+
return readFileSync2(join2(TEMPLATES_DIR, relativePath), "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/modules/linkedin/index.ts
|
|
53
|
+
var LI_COMMANDS = [
|
|
54
|
+
"li-all.md",
|
|
55
|
+
"li-about.md",
|
|
56
|
+
"li-headline.md",
|
|
57
|
+
"li-experience.md",
|
|
58
|
+
"li-skills.md",
|
|
59
|
+
"li-featured.md",
|
|
60
|
+
"li-activity.md"
|
|
61
|
+
];
|
|
62
|
+
var LI_MANAGED_PATHS = [
|
|
63
|
+
...LI_COMMANDS.map((c) => `.claude/commands/${c}`),
|
|
64
|
+
".claude/modules/linkedin/manifest.md"
|
|
65
|
+
];
|
|
66
|
+
var CONTENT_FILE_NAMES = [
|
|
67
|
+
"INSTRUCTIONS.md",
|
|
68
|
+
"LinkedIn Current State.md",
|
|
69
|
+
"LinkedIn About.md",
|
|
70
|
+
"LinkedIn Headline.md",
|
|
71
|
+
"LinkedIn Experience.md",
|
|
72
|
+
"LinkedIn Skills.md",
|
|
73
|
+
"LinkedIn Featured.md",
|
|
74
|
+
"LinkedIn Activity.md"
|
|
75
|
+
];
|
|
76
|
+
var linkedinModule = {
|
|
77
|
+
id: "linkedin",
|
|
78
|
+
label: "LinkedIn",
|
|
79
|
+
hint: "draft and refine your LinkedIn profile",
|
|
80
|
+
managedPaths: LI_MANAGED_PATHS,
|
|
81
|
+
contentFileNames: CONTENT_FILE_NAMES,
|
|
82
|
+
managedFiles(vars) {
|
|
83
|
+
const files = LI_COMMANDS.map((cmd2) => [
|
|
84
|
+
`.claude/commands/${cmd2}`,
|
|
85
|
+
render(tpl(`modules/linkedin/commands/${cmd2}`), vars)
|
|
86
|
+
]);
|
|
87
|
+
files.push([
|
|
88
|
+
".claude/modules/linkedin/manifest.md",
|
|
89
|
+
render(tpl("modules/linkedin/manifest.md"), vars)
|
|
90
|
+
]);
|
|
91
|
+
return files;
|
|
92
|
+
},
|
|
93
|
+
contentFiles(vars, contentDir) {
|
|
94
|
+
return CONTENT_FILE_NAMES.map((file) => [
|
|
95
|
+
`${contentDir}/linkedin/${file}`,
|
|
96
|
+
render(tpl(`modules/linkedin/graph/${file}`), vars)
|
|
97
|
+
]);
|
|
98
|
+
},
|
|
99
|
+
onAdd(profile, inputs) {
|
|
100
|
+
if (!profile.linkedin?.profile_url && inputs.liProfileUrl?.trim()) {
|
|
101
|
+
return { ...profile, linkedin: { profile_url: inputs.liProfileUrl.trim() } };
|
|
102
|
+
}
|
|
103
|
+
return profile;
|
|
104
|
+
},
|
|
105
|
+
onRemove(profile) {
|
|
106
|
+
const updated = { ...profile };
|
|
107
|
+
delete updated.linkedin;
|
|
108
|
+
return updated;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/modules/resume/index.ts
|
|
113
|
+
var RESUME_MANAGED_PATHS = [
|
|
114
|
+
".claude/commands/resume-refresh.md",
|
|
115
|
+
".claude/modules/resume/manifest.md"
|
|
116
|
+
];
|
|
117
|
+
var CONTENT_FILE_NAMES2 = [
|
|
118
|
+
"INSTRUCTIONS.md",
|
|
119
|
+
"Resume Working Draft.md",
|
|
120
|
+
"Resume Last Submitted.md"
|
|
121
|
+
];
|
|
122
|
+
var resumeModule = {
|
|
123
|
+
id: "resume",
|
|
124
|
+
label: "Resume",
|
|
125
|
+
hint: "keep your resume current from your graph",
|
|
126
|
+
managedPaths: RESUME_MANAGED_PATHS,
|
|
127
|
+
contentFileNames: CONTENT_FILE_NAMES2,
|
|
128
|
+
managedFiles(vars) {
|
|
129
|
+
const [commandPath, manifestPath] = RESUME_MANAGED_PATHS;
|
|
130
|
+
return [
|
|
131
|
+
[commandPath, render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
|
|
132
|
+
[manifestPath, render(tpl("modules/resume/manifest.md"), vars)]
|
|
133
|
+
];
|
|
134
|
+
},
|
|
135
|
+
contentFiles(vars, contentDir) {
|
|
136
|
+
return CONTENT_FILE_NAMES2.map((file) => [
|
|
137
|
+
`${contentDir}/resume/${file}`,
|
|
138
|
+
render(tpl(`modules/resume/graph/${file}`), vars)
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
// Resume has no module-specific profile fields — no onAdd/onRemove needed.
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/modules/registry.ts
|
|
145
|
+
var MODULES = [linkedinModule, resumeModule];
|
|
146
|
+
var BY_ID = new Map(MODULES.map((m) => [m.id, m]));
|
|
147
|
+
function getModule(id) {
|
|
148
|
+
return BY_ID.get(id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/checksums.ts
|
|
42
152
|
function hashContent(content) {
|
|
43
153
|
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
44
154
|
}
|
|
45
155
|
function hashFile(filePath) {
|
|
46
156
|
if (!existsSync2(filePath)) return null;
|
|
47
|
-
return hashContent(
|
|
157
|
+
return hashContent(readFileSync3(filePath, "utf8"));
|
|
48
158
|
}
|
|
49
159
|
var CONTENT_SUBDIRS = ["notes", "skills", "posts"];
|
|
50
|
-
var
|
|
51
|
-
|
|
52
|
-
".claude/commands/li-about.md",
|
|
53
|
-
".claude/commands/li-headline.md",
|
|
54
|
-
".claude/commands/li-experience.md",
|
|
55
|
-
".claude/commands/li-skills.md",
|
|
56
|
-
".claude/commands/li-featured.md",
|
|
57
|
-
".claude/commands/li-activity.md"
|
|
58
|
-
];
|
|
59
|
-
var RESUME_MANAGED_FILES = [
|
|
60
|
-
".claude/commands/resume-refresh.md"
|
|
61
|
-
];
|
|
62
|
-
var MODULE_MANAGED_FILES = {
|
|
63
|
-
linkedin: [
|
|
64
|
-
...LINKEDIN_MANAGED_FILES,
|
|
65
|
-
".claude/modules/linkedin/manifest.md"
|
|
66
|
-
],
|
|
67
|
-
resume: [
|
|
68
|
-
...RESUME_MANAGED_FILES,
|
|
69
|
-
".claude/modules/resume/manifest.md"
|
|
70
|
-
]
|
|
71
|
-
};
|
|
72
|
-
var MODULE_CONTENT_FILES = {
|
|
73
|
-
linkedin: [
|
|
74
|
-
"INSTRUCTIONS.md",
|
|
75
|
-
"LinkedIn Current State.md",
|
|
76
|
-
"LinkedIn About.md",
|
|
77
|
-
"LinkedIn Headline.md",
|
|
78
|
-
"LinkedIn Experience.md",
|
|
79
|
-
"LinkedIn Skills.md",
|
|
80
|
-
"LinkedIn Featured.md",
|
|
81
|
-
"LinkedIn Activity.md"
|
|
82
|
-
],
|
|
83
|
-
resume: [
|
|
84
|
-
"INSTRUCTIONS.md",
|
|
85
|
-
"Resume Working Draft.md",
|
|
86
|
-
"Resume Last Submitted.md"
|
|
87
|
-
]
|
|
88
|
-
};
|
|
160
|
+
var MODULE_MANAGED_FILES = Object.fromEntries(MODULES.map((m) => [m.id, m.managedPaths]));
|
|
161
|
+
var MODULE_CONTENT_FILES = Object.fromEntries(MODULES.map((m) => [m.id, m.contentFileNames]));
|
|
89
162
|
|
|
90
163
|
// src/upgrade.ts
|
|
91
164
|
function writeManagedFile(targetDir, relativePath, newContent, storedChecksums) {
|
|
92
|
-
const fullPath =
|
|
165
|
+
const fullPath = join3(targetDir, relativePath);
|
|
93
166
|
const newChecksum = hashContent(newContent);
|
|
94
167
|
if (!existsSync3(fullPath)) {
|
|
95
|
-
mkdirSync(
|
|
168
|
+
mkdirSync(dirname2(fullPath), { recursive: true });
|
|
96
169
|
writeFileSync(fullPath, newContent, "utf8");
|
|
97
170
|
return { outcome: "added", checksum: newChecksum };
|
|
98
171
|
}
|
|
@@ -105,21 +178,66 @@ function writeManagedFile(targetDir, relativePath, newContent, storedChecksums)
|
|
|
105
178
|
return { outcome: "skipped", checksum: storedHash };
|
|
106
179
|
}
|
|
107
180
|
|
|
108
|
-
// src/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
181
|
+
// src/state.ts
|
|
182
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
183
|
+
import { join as join4 } from "path";
|
|
184
|
+
var STATE_FILENAME = ".patina-state.json";
|
|
185
|
+
function normalizeChecksums(checksums) {
|
|
186
|
+
const result = {};
|
|
187
|
+
for (const [key, value] of Object.entries(checksums)) {
|
|
188
|
+
result[key.replace(/\\/g, "/")] = value;
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function readState(root, profile) {
|
|
193
|
+
const statePath = join4(root, STATE_FILENAME);
|
|
194
|
+
if (existsSync4(statePath)) {
|
|
195
|
+
const raw = readFileSync4(statePath, "utf8");
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Corrupt ${STATE_FILENAME}: failed to parse JSON. Fix or delete the file at ${statePath} and try again.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const obj = parsed;
|
|
205
|
+
const rawChecksums = obj.checksums;
|
|
206
|
+
if (rawChecksums !== void 0 && (typeof rawChecksums !== "object" || Array.isArray(rawChecksums) || rawChecksums === null)) {
|
|
207
|
+
throw new Error(`Corrupt ${STATE_FILENAME}: 'checksums' must be an object at ${statePath}.`);
|
|
208
|
+
}
|
|
209
|
+
const checksums2 = normalizeChecksums(rawChecksums ?? {});
|
|
210
|
+
return { checksums: checksums2 };
|
|
211
|
+
}
|
|
212
|
+
const legacyChecksums = profile?._checksums;
|
|
213
|
+
const checksums = normalizeChecksums(legacyChecksums ?? {});
|
|
214
|
+
return { checksums };
|
|
215
|
+
}
|
|
216
|
+
function stripLegacyChecksums(profile) {
|
|
217
|
+
const { _checksums: _stripped, ...clean } = profile;
|
|
218
|
+
return clean;
|
|
219
|
+
}
|
|
220
|
+
function writeState(root, state) {
|
|
221
|
+
const normalized = {
|
|
222
|
+
checksums: normalizeChecksums(state.checksums)
|
|
223
|
+
};
|
|
224
|
+
writeFileSync2(
|
|
225
|
+
join4(root, STATE_FILENAME),
|
|
226
|
+
JSON.stringify(normalized, null, 2) + "\n",
|
|
227
|
+
"utf8"
|
|
228
|
+
);
|
|
113
229
|
}
|
|
230
|
+
|
|
231
|
+
// src/scaffold.ts
|
|
114
232
|
function writeRaw(targetDir, relativePath, content) {
|
|
115
|
-
const full =
|
|
116
|
-
mkdirSync2(
|
|
117
|
-
|
|
233
|
+
const full = join5(targetDir, relativePath);
|
|
234
|
+
mkdirSync2(dirname3(full), { recursive: true });
|
|
235
|
+
writeFileSync3(full, content, "utf8");
|
|
118
236
|
}
|
|
119
237
|
function touch(targetDir, relativePath) {
|
|
120
|
-
const full =
|
|
121
|
-
mkdirSync2(
|
|
122
|
-
|
|
238
|
+
const full = join5(targetDir, relativePath);
|
|
239
|
+
mkdirSync2(dirname3(full), { recursive: true });
|
|
240
|
+
writeFileSync3(full, "", "utf8");
|
|
123
241
|
}
|
|
124
242
|
function profileToVars(profile, liProfileUrl) {
|
|
125
243
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -148,7 +266,7 @@ function baseManagedFiles(vars, editor, targetDir) {
|
|
|
148
266
|
mcpServers: {
|
|
149
267
|
obsidian: {
|
|
150
268
|
command: "npx",
|
|
151
|
-
args: ["-y", "mcp-obsidian@latest",
|
|
269
|
+
args: ["-y", "mcp-obsidian@latest", join5(targetDir, vars.CONTENT_DIR).replace(/\\/g, "/")]
|
|
152
270
|
}
|
|
153
271
|
}
|
|
154
272
|
};
|
|
@@ -157,50 +275,10 @@ function baseManagedFiles(vars, editor, targetDir) {
|
|
|
157
275
|
return files;
|
|
158
276
|
}
|
|
159
277
|
function moduleManagedFiles(module, vars) {
|
|
160
|
-
|
|
161
|
-
const liCmds = [
|
|
162
|
-
"li-all.md",
|
|
163
|
-
"li-about.md",
|
|
164
|
-
"li-headline.md",
|
|
165
|
-
"li-experience.md",
|
|
166
|
-
"li-skills.md",
|
|
167
|
-
"li-featured.md",
|
|
168
|
-
"li-activity.md"
|
|
169
|
-
];
|
|
170
|
-
const files = liCmds.map((cmd2) => [
|
|
171
|
-
`.claude/commands/${cmd2}`,
|
|
172
|
-
render(tpl(`modules/linkedin/commands/${cmd2}`), vars)
|
|
173
|
-
]);
|
|
174
|
-
files.push([
|
|
175
|
-
".claude/modules/linkedin/manifest.md",
|
|
176
|
-
render(tpl("modules/linkedin/manifest.md"), vars)
|
|
177
|
-
]);
|
|
178
|
-
return files;
|
|
179
|
-
}
|
|
180
|
-
if (module === "resume") {
|
|
181
|
-
return [
|
|
182
|
-
[".claude/commands/resume-refresh.md", render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
|
|
183
|
-
[".claude/modules/resume/manifest.md", render(tpl("modules/resume/manifest.md"), vars)]
|
|
184
|
-
];
|
|
185
|
-
}
|
|
186
|
-
return [];
|
|
278
|
+
return getModule(module)?.managedFiles(vars) ?? [];
|
|
187
279
|
}
|
|
188
280
|
function moduleContentFiles(module, vars, contentDir) {
|
|
189
|
-
|
|
190
|
-
const files = MODULE_CONTENT_FILES["linkedin"] ?? [];
|
|
191
|
-
return files.map((file) => [
|
|
192
|
-
`${contentDir}/linkedin/${file}`,
|
|
193
|
-
render(tpl(`modules/linkedin/graph/${file}`), vars)
|
|
194
|
-
]);
|
|
195
|
-
}
|
|
196
|
-
if (module === "resume") {
|
|
197
|
-
const files = MODULE_CONTENT_FILES["resume"] ?? [];
|
|
198
|
-
return files.map((file) => [
|
|
199
|
-
`${contentDir}/resume/${file}`,
|
|
200
|
-
render(tpl(`modules/resume/graph/${file}`), vars)
|
|
201
|
-
]);
|
|
202
|
-
}
|
|
203
|
-
return [];
|
|
281
|
+
return getModule(module)?.contentFiles(vars, contentDir) ?? [];
|
|
204
282
|
}
|
|
205
283
|
async function scaffold(opts) {
|
|
206
284
|
const {
|
|
@@ -263,28 +341,28 @@ async function scaffold(opts) {
|
|
|
263
341
|
writeRaw(targetDir, relativePath, content);
|
|
264
342
|
}
|
|
265
343
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
344
|
+
writeRaw(targetDir, "profile.yaml", yaml2.dump(tempProfile));
|
|
345
|
+
writeState(targetDir, { checksums });
|
|
346
|
+
writeRaw(targetDir, ".gitignore", `.obsidian/
|
|
347
|
+
.DS_Store
|
|
348
|
+
${STATE_FILENAME}
|
|
349
|
+
`);
|
|
272
350
|
}
|
|
273
351
|
|
|
274
352
|
// src/validate.ts
|
|
275
|
-
import { existsSync as
|
|
276
|
-
import { join as
|
|
353
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync } from "fs";
|
|
354
|
+
import { join as join6, relative, sep, basename } from "path";
|
|
277
355
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
278
356
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
279
357
|
var POSTS = CONTENT_SUBDIRS[2];
|
|
280
358
|
function findPatinaRoot(cwd) {
|
|
281
|
-
return
|
|
359
|
+
return existsSync5(join6(cwd, "profile.yaml")) ? cwd : null;
|
|
282
360
|
}
|
|
283
361
|
function listMarkdownFiles(dir) {
|
|
284
|
-
if (!
|
|
362
|
+
if (!existsSync5(dir)) return [];
|
|
285
363
|
const results = [];
|
|
286
364
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
287
|
-
const fullPath =
|
|
365
|
+
const fullPath = join6(dir, entry.name);
|
|
288
366
|
if (entry.isDirectory()) {
|
|
289
367
|
results.push(...listMarkdownFiles(fullPath));
|
|
290
368
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -337,14 +415,14 @@ function parseExclusions(markdown) {
|
|
|
337
415
|
return [...new Set(items)];
|
|
338
416
|
}
|
|
339
417
|
function checkSkillNotes(root, profile) {
|
|
340
|
-
const contentDir =
|
|
341
|
-
const notesDir =
|
|
342
|
-
const skillsDir =
|
|
418
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
419
|
+
const notesDir = join6(contentDir, NOTES);
|
|
420
|
+
const skillsDir = join6(contentDir, SKILLS);
|
|
343
421
|
const noteFiles = listMarkdownFiles(notesDir);
|
|
344
422
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
345
423
|
const issues = [];
|
|
346
424
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
347
|
-
const content =
|
|
425
|
+
const content = readFileSync5(skillFile, "utf8");
|
|
348
426
|
const links = extractWikiLinks(content);
|
|
349
427
|
for (const { target, line } of links) {
|
|
350
428
|
if (!noteSlugs.has(target)) {
|
|
@@ -360,9 +438,9 @@ function checkSkillNotes(root, profile) {
|
|
|
360
438
|
return issues;
|
|
361
439
|
}
|
|
362
440
|
function checkWikiLinks(root, profile) {
|
|
363
|
-
const contentDir =
|
|
364
|
-
const notesDir =
|
|
365
|
-
const postsDir =
|
|
441
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
442
|
+
const notesDir = join6(contentDir, NOTES);
|
|
443
|
+
const postsDir = join6(contentDir, POSTS);
|
|
366
444
|
const noteFiles = listMarkdownFiles(notesDir);
|
|
367
445
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
368
446
|
const issues = [];
|
|
@@ -371,7 +449,7 @@ function checkWikiLinks(root, profile) {
|
|
|
371
449
|
...listMarkdownFiles(postsDir)
|
|
372
450
|
];
|
|
373
451
|
for (const file of filesToScan) {
|
|
374
|
-
const content =
|
|
452
|
+
const content = readFileSync5(file, "utf8");
|
|
375
453
|
const links = extractWikiLinks(content);
|
|
376
454
|
for (const { target, line } of links) {
|
|
377
455
|
if (!noteSlugs.has(target)) {
|
|
@@ -387,15 +465,15 @@ function checkWikiLinks(root, profile) {
|
|
|
387
465
|
return issues;
|
|
388
466
|
}
|
|
389
467
|
function checkExclusions(root, profile) {
|
|
390
|
-
const contentDir =
|
|
391
|
-
const notesDir =
|
|
392
|
-
const exclusionsPath =
|
|
393
|
-
if (!
|
|
394
|
-
const content =
|
|
468
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
469
|
+
const notesDir = join6(contentDir, NOTES);
|
|
470
|
+
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
471
|
+
if (!existsSync5(exclusionsPath)) return [];
|
|
472
|
+
const content = readFileSync5(exclusionsPath, "utf8");
|
|
395
473
|
const items = parseExclusions(content);
|
|
396
474
|
if (items.length === 0) return [];
|
|
397
|
-
const skillsDir =
|
|
398
|
-
const postsDir =
|
|
475
|
+
const skillsDir = join6(contentDir, SKILLS);
|
|
476
|
+
const postsDir = join6(contentDir, POSTS);
|
|
399
477
|
const filesToScan = [
|
|
400
478
|
...listMarkdownFiles(skillsDir),
|
|
401
479
|
...listMarkdownFiles(postsDir)
|
|
@@ -403,7 +481,7 @@ function checkExclusions(root, profile) {
|
|
|
403
481
|
const issues = [];
|
|
404
482
|
const seen = /* @__PURE__ */ new Set();
|
|
405
483
|
for (const file of filesToScan) {
|
|
406
|
-
const fileContent =
|
|
484
|
+
const fileContent = readFileSync5(file, "utf8");
|
|
407
485
|
const lines = fileContent.split("\n");
|
|
408
486
|
for (let i = 0; i < lines.length; i++) {
|
|
409
487
|
const lineText = lines[i];
|
|
@@ -436,11 +514,11 @@ function validate(root, profile) {
|
|
|
436
514
|
if (a.file > b.file) return 1;
|
|
437
515
|
return (a.line ?? 0) - (b.line ?? 0);
|
|
438
516
|
});
|
|
439
|
-
const contentDir =
|
|
517
|
+
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
440
518
|
const scannedFiles = /* @__PURE__ */ new Set([
|
|
441
|
-
...listMarkdownFiles(
|
|
442
|
-
...listMarkdownFiles(
|
|
443
|
-
...listMarkdownFiles(
|
|
519
|
+
...listMarkdownFiles(join6(contentDir, NOTES)),
|
|
520
|
+
...listMarkdownFiles(join6(contentDir, SKILLS)),
|
|
521
|
+
...listMarkdownFiles(join6(contentDir, POSTS))
|
|
444
522
|
]);
|
|
445
523
|
return {
|
|
446
524
|
ok: allIssues.length === 0,
|
|
@@ -574,18 +652,11 @@ async function runInstall(cwd) {
|
|
|
574
652
|
}),
|
|
575
653
|
modules: () => p.multiselect({
|
|
576
654
|
message: `Which modules do you want to add?${MULTISELECT_HINT}`,
|
|
577
|
-
options:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
value: "resume",
|
|
585
|
-
label: "Resume",
|
|
586
|
-
hint: chalk.hex("#64748B")("keep your resume current from your graph")
|
|
587
|
-
}
|
|
588
|
-
],
|
|
655
|
+
options: MODULES.map((m) => ({
|
|
656
|
+
value: m.id,
|
|
657
|
+
label: m.label,
|
|
658
|
+
hint: chalk.hex("#64748B")(m.hint)
|
|
659
|
+
})),
|
|
589
660
|
required: false
|
|
590
661
|
})
|
|
591
662
|
},
|
|
@@ -641,12 +712,12 @@ async function runInstall(cwd) {
|
|
|
641
712
|
p.outro(chalk.hex("#94A3B8")("Run claude from inside your patina to get started."));
|
|
642
713
|
}
|
|
643
714
|
function writeProfile(cwd, profile) {
|
|
644
|
-
const full =
|
|
645
|
-
|
|
715
|
+
const full = join7(cwd, "profile.yaml");
|
|
716
|
+
writeFileSync4(full, yaml3.dump(profile), "utf8");
|
|
646
717
|
}
|
|
647
718
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
648
|
-
const fullPath =
|
|
649
|
-
if (!
|
|
719
|
+
const fullPath = join7(targetDir, rel);
|
|
720
|
+
if (!existsSync6(fullPath)) return "deleted";
|
|
650
721
|
const currentHash = hashFile(fullPath);
|
|
651
722
|
const storedHash = stored[rel];
|
|
652
723
|
if (storedHash && currentHash !== storedHash) {
|
|
@@ -688,6 +759,48 @@ async function runUpdate(cwd) {
|
|
|
688
759
|
await runValidate(cwd, profile);
|
|
689
760
|
}
|
|
690
761
|
}
|
|
762
|
+
function applyProfileUpdate(cwd, profile, fields) {
|
|
763
|
+
const updatedProfile = {
|
|
764
|
+
...profile,
|
|
765
|
+
name: fields.name.trim(),
|
|
766
|
+
title: fields.title.trim(),
|
|
767
|
+
role_description: fields.roleDescription.trim() || void 0,
|
|
768
|
+
job_description_url: fields.jobDescriptionUrl.trim() || void 0,
|
|
769
|
+
work: {
|
|
770
|
+
self_employed: fields.selfEmployed,
|
|
771
|
+
company_name: fields.companyName.trim() || (fields.selfEmployed ? "Freelance" : ""),
|
|
772
|
+
website: fields.website.trim() || void 0,
|
|
773
|
+
company_description: fields.companyDescription.trim() || void 0
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
const vars = profileToVars(updatedProfile);
|
|
777
|
+
const stored = readState(cwd, profile).checksums;
|
|
778
|
+
const newChecksums = {};
|
|
779
|
+
const files = [
|
|
780
|
+
...baseManagedFiles(vars, updatedProfile.editor, cwd),
|
|
781
|
+
...updatedProfile.modules.flatMap((m) => moduleManagedFiles(m, vars))
|
|
782
|
+
];
|
|
783
|
+
const updated = [];
|
|
784
|
+
const skipped = [];
|
|
785
|
+
for (const [rel, content] of files) {
|
|
786
|
+
const { outcome, checksum } = writeManagedFile(cwd, rel, content, stored);
|
|
787
|
+
newChecksums[rel] = checksum;
|
|
788
|
+
if (outcome === "skipped") {
|
|
789
|
+
skipped.push(rel);
|
|
790
|
+
} else {
|
|
791
|
+
updated.push(rel);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
for (const [rel, hash] of Object.entries(stored)) {
|
|
795
|
+
if (!(rel in newChecksums)) {
|
|
796
|
+
newChecksums[rel] = hash;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
writeState(cwd, { checksums: newChecksums });
|
|
800
|
+
const profileToWrite = stripLegacyChecksums(updatedProfile);
|
|
801
|
+
writeProfile(cwd, profileToWrite);
|
|
802
|
+
return { profile: profileToWrite, updated, skipped };
|
|
803
|
+
}
|
|
691
804
|
async function runUpdateProfile(cwd, profile) {
|
|
692
805
|
console.log("");
|
|
693
806
|
console.log(` ${label("Update personal info")}`);
|
|
@@ -739,44 +852,16 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
739
852
|
},
|
|
740
853
|
{ onCancel }
|
|
741
854
|
);
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
company_description: work.companyDescription?.trim() || void 0
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
const vars = profileToVars(updatedProfile);
|
|
756
|
-
const stored = profile._checksums ?? {};
|
|
757
|
-
const newChecksums = {};
|
|
758
|
-
const files = [
|
|
759
|
-
...baseManagedFiles(vars, updatedProfile.editor, cwd),
|
|
760
|
-
...updatedProfile.modules.flatMap((m) => moduleManagedFiles(m, vars))
|
|
761
|
-
];
|
|
762
|
-
const updated = [];
|
|
763
|
-
const skipped = [];
|
|
764
|
-
for (const [rel, content] of files) {
|
|
765
|
-
const { outcome, checksum } = writeManagedFile(cwd, rel, content, stored);
|
|
766
|
-
newChecksums[rel] = checksum;
|
|
767
|
-
if (outcome === "skipped") {
|
|
768
|
-
skipped.push(rel);
|
|
769
|
-
} else {
|
|
770
|
-
updated.push(rel);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
for (const [rel, hash] of Object.entries(stored)) {
|
|
774
|
-
if (!(rel in newChecksums)) {
|
|
775
|
-
newChecksums[rel] = hash;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
updatedProfile._checksums = newChecksums;
|
|
779
|
-
writeProfile(cwd, updatedProfile);
|
|
855
|
+
const { updated, skipped } = applyProfileUpdate(cwd, profile, {
|
|
856
|
+
name: identity.name,
|
|
857
|
+
title: identity.title ?? "",
|
|
858
|
+
roleDescription: identity.roleDescription ?? "",
|
|
859
|
+
jobDescriptionUrl: identity.jobDescriptionUrl ?? "",
|
|
860
|
+
selfEmployed,
|
|
861
|
+
companyName: work.companyName ?? "",
|
|
862
|
+
website: work.website ?? "",
|
|
863
|
+
companyDescription: work.companyDescription ?? ""
|
|
864
|
+
});
|
|
780
865
|
const summaryLines = [];
|
|
781
866
|
if (updated.length > 0) {
|
|
782
867
|
summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
|
|
@@ -787,96 +872,102 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
787
872
|
p.note(summaryLines.join("\n"), label("Done"));
|
|
788
873
|
p.outro(chalk.hex("#94A3B8")("Profile updated."));
|
|
789
874
|
}
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
const selected = await p.multiselect({
|
|
793
|
-
message: `Which modules do you want active?${MULTISELECT_HINT}`,
|
|
794
|
-
options: [
|
|
795
|
-
{
|
|
796
|
-
value: "linkedin",
|
|
797
|
-
label: "LinkedIn",
|
|
798
|
-
hint: chalk.hex("#64748B")("draft and refine your LinkedIn profile")
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
value: "resume",
|
|
802
|
-
label: "Resume",
|
|
803
|
-
hint: chalk.hex("#64748B")("keep your resume current from your graph")
|
|
804
|
-
}
|
|
805
|
-
],
|
|
806
|
-
initialValues: currentModules,
|
|
807
|
-
required: false
|
|
808
|
-
});
|
|
809
|
-
if (p.isCancel(selected)) {
|
|
810
|
-
p.cancel(chalk.hex("#94A3B8")("No changes made."));
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
const selectedModules = Array.isArray(selected) ? selected : [];
|
|
814
|
-
const toAdd = selectedModules.filter((m) => !currentModules.includes(m));
|
|
815
|
-
const toRemove = currentModules.filter((m) => !selectedModules.includes(m));
|
|
816
|
-
if (toAdd.length === 0 && toRemove.length === 0) {
|
|
817
|
-
p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const stored = profile._checksums ?? {};
|
|
875
|
+
function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
876
|
+
const stored = readState(cwd, profile).checksums;
|
|
821
877
|
const newChecksums = { ...stored };
|
|
822
|
-
|
|
823
|
-
const
|
|
878
|
+
let updatedProfile = { ...profile, modules: [...profile.modules ?? []] };
|
|
879
|
+
const added = [];
|
|
824
880
|
const skippedFiles = [];
|
|
825
|
-
const
|
|
826
|
-
const
|
|
881
|
+
const deleted = [];
|
|
882
|
+
const kept = [];
|
|
827
883
|
for (const module of toAdd) {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
placeholder: "https://linkedin.com/in/yourname (optional)"
|
|
832
|
-
});
|
|
833
|
-
if (p.isCancel(url)) {
|
|
834
|
-
p.cancel(chalk.hex("#94A3B8")("No changes made."));
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
if (typeof url === "string" && url.trim()) {
|
|
838
|
-
updatedProfile.linkedin = { profile_url: url.trim() };
|
|
839
|
-
}
|
|
884
|
+
const def = getModule(module);
|
|
885
|
+
if (def?.onAdd) {
|
|
886
|
+
updatedProfile = def.onAdd(updatedProfile, moduleInputs?.[module] ?? {});
|
|
840
887
|
}
|
|
841
888
|
const vars = profileToVars(updatedProfile);
|
|
842
889
|
const contentDir = updatedProfile.content_dir;
|
|
843
890
|
for (const [rel, content] of moduleManagedFiles(module, vars)) {
|
|
844
|
-
const { outcome, checksum } = writeManagedFile(cwd, rel, content,
|
|
891
|
+
const { outcome, checksum } = writeManagedFile(cwd, rel, content, newChecksums);
|
|
845
892
|
newChecksums[rel] = checksum;
|
|
846
893
|
if (outcome === "skipped") {
|
|
847
894
|
skippedFiles.push(rel);
|
|
848
895
|
} else {
|
|
849
|
-
|
|
896
|
+
added.push(rel);
|
|
850
897
|
}
|
|
851
898
|
}
|
|
852
899
|
for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
|
|
853
|
-
const fullPath =
|
|
854
|
-
if (!
|
|
855
|
-
mkdirSync3(
|
|
856
|
-
|
|
857
|
-
|
|
900
|
+
const fullPath = join7(cwd, relativePath);
|
|
901
|
+
if (!existsSync6(fullPath)) {
|
|
902
|
+
mkdirSync3(dirname4(fullPath), { recursive: true });
|
|
903
|
+
writeFileSync4(fullPath, content, "utf8");
|
|
904
|
+
added.push(relativePath);
|
|
858
905
|
}
|
|
859
906
|
}
|
|
860
|
-
updatedProfile.modules
|
|
907
|
+
if (!updatedProfile.modules.includes(module)) {
|
|
908
|
+
updatedProfile.modules = [...updatedProfile.modules, module];
|
|
909
|
+
}
|
|
861
910
|
}
|
|
862
911
|
for (const module of toRemove) {
|
|
863
|
-
const
|
|
912
|
+
const def = getModule(module);
|
|
913
|
+
const managedRels = def?.managedPaths ?? [];
|
|
864
914
|
for (const rel of managedRels) {
|
|
865
915
|
const result = removeManagedFileIfUnmodified(cwd, rel, stored);
|
|
866
916
|
if (result === "deleted") {
|
|
867
|
-
|
|
917
|
+
deleted.push(rel);
|
|
868
918
|
delete newChecksums[rel];
|
|
869
919
|
} else {
|
|
870
|
-
|
|
920
|
+
kept.push(rel);
|
|
871
921
|
}
|
|
872
922
|
}
|
|
873
923
|
updatedProfile.modules = updatedProfile.modules.filter((m) => m !== module);
|
|
874
|
-
if (
|
|
875
|
-
|
|
924
|
+
if (def?.onRemove) {
|
|
925
|
+
updatedProfile = def.onRemove(updatedProfile);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
writeState(cwd, { checksums: newChecksums });
|
|
929
|
+
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
930
|
+
writeProfile(cwd, finalProfile);
|
|
931
|
+
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept };
|
|
932
|
+
}
|
|
933
|
+
async function runUpdateModules(cwd, profile) {
|
|
934
|
+
const currentModules = profile.modules ?? [];
|
|
935
|
+
const selected = await p.multiselect({
|
|
936
|
+
message: `Which modules do you want active?${MULTISELECT_HINT}`,
|
|
937
|
+
options: MODULES.map((m) => ({
|
|
938
|
+
value: m.id,
|
|
939
|
+
label: m.label,
|
|
940
|
+
hint: chalk.hex("#64748B")(m.hint)
|
|
941
|
+
})),
|
|
942
|
+
initialValues: currentModules,
|
|
943
|
+
required: false
|
|
944
|
+
});
|
|
945
|
+
if (p.isCancel(selected)) {
|
|
946
|
+
p.cancel(chalk.hex("#94A3B8")("No changes made."));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const selectedModules = Array.isArray(selected) ? selected : [];
|
|
950
|
+
const toAdd = selectedModules.filter((m) => !currentModules.includes(m));
|
|
951
|
+
const toRemove = currentModules.filter((m) => !selectedModules.includes(m));
|
|
952
|
+
if (toAdd.length === 0 && toRemove.length === 0) {
|
|
953
|
+
p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
let liProfileUrl;
|
|
957
|
+
if (toAdd.includes("linkedin") && !profile.linkedin?.profile_url) {
|
|
958
|
+
const url = await p.text({
|
|
959
|
+
message: "What's your LinkedIn profile URL?",
|
|
960
|
+
placeholder: "https://linkedin.com/in/yourname (optional)"
|
|
961
|
+
});
|
|
962
|
+
if (p.isCancel(url)) {
|
|
963
|
+
p.cancel(chalk.hex("#94A3B8")("No changes made."));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (typeof url === "string" && url.trim()) {
|
|
967
|
+
liProfileUrl = url.trim();
|
|
876
968
|
}
|
|
877
969
|
}
|
|
878
|
-
|
|
879
|
-
writeProfile(cwd, updatedProfile);
|
|
970
|
+
const { added: addedFiles, skipped: skippedFiles, deleted: deletedFiles, kept: keptFiles } = applyModuleChanges(cwd, profile, toAdd, toRemove, { linkedin: { liProfileUrl } });
|
|
880
971
|
const summaryLines = [];
|
|
881
972
|
if (addedFiles.length > 0) summaryLines.push(chalk.hex("#94A3B8")(`Added: ${addedFiles.join(", ")}`));
|
|
882
973
|
if (skippedFiles.length > 0) summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skippedFiles.join(", ")}`));
|