heyiam 0.2.29 → 0.3.1
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/README.md +45 -0
- package/dist/auth.js +29 -3
- package/dist/config.js +10 -1
- package/dist/db.js +0 -1
- package/dist/export.js +124 -27
- package/dist/format-utils.js +5 -0
- package/dist/github.js +381 -0
- package/dist/index.js +168 -0
- package/dist/mount.js +300 -102
- package/dist/parsers/claude.js +2 -28
- package/dist/parsers/codex.js +2 -26
- package/dist/parsers/cursor.js +2 -26
- package/dist/parsers/duration.js +35 -0
- package/dist/parsers/gemini.js +2 -20
- package/dist/parsers/index.js +22 -3
- package/dist/parsers/types.js +0 -1
- package/dist/public/assets/index-Coilyhtr.css +1 -0
- package/dist/public/assets/index-D0noVMFu.js +44 -0
- package/dist/public/index.html +2 -2
- package/dist/redact.js +4 -104
- package/dist/render/build-render-data.js +9 -2
- package/dist/render/index.js +32 -5
- package/dist/render/liquid.js +147 -7
- package/dist/render/mock-data.js +303 -0
- package/dist/render/templates/aurora/portfolio.liquid +192 -0
- package/dist/render/templates/aurora/project.liquid +260 -0
- package/dist/render/templates/aurora/session.liquid +223 -0
- package/dist/render/templates/aurora/styles.css +1184 -0
- package/dist/render/templates/bauhaus/portfolio.liquid +169 -0
- package/dist/render/templates/bauhaus/project.liquid +300 -0
- package/dist/render/templates/bauhaus/session.liquid +333 -0
- package/dist/render/templates/bauhaus/styles.css +1645 -0
- package/dist/render/templates/blueprint/portfolio.liquid +153 -0
- package/dist/render/templates/blueprint/project.liquid +286 -0
- package/dist/render/templates/blueprint/session.liquid +248 -0
- package/dist/render/templates/blueprint/styles.css +1289 -0
- package/dist/render/templates/canvas/portfolio.liquid +203 -0
- package/dist/render/templates/canvas/project.liquid +235 -0
- package/dist/render/templates/canvas/session.liquid +223 -0
- package/dist/render/templates/canvas/styles.css +1440 -0
- package/dist/render/templates/carbon/portfolio.liquid +160 -0
- package/dist/render/templates/carbon/project.liquid +249 -0
- package/dist/render/templates/carbon/session.liquid +190 -0
- package/dist/render/templates/carbon/styles.css +1097 -0
- package/dist/render/templates/chalk/portfolio.liquid +189 -0
- package/dist/render/templates/chalk/project.liquid +245 -0
- package/dist/render/templates/chalk/session.liquid +215 -0
- package/dist/render/templates/chalk/styles.css +1161 -0
- package/dist/render/templates/circuit/portfolio.liquid +152 -0
- package/dist/render/templates/circuit/project.liquid +247 -0
- package/dist/render/templates/circuit/session.liquid +205 -0
- package/dist/render/templates/circuit/styles.css +1409 -0
- package/dist/render/templates/cosmos/portfolio.liquid +222 -0
- package/dist/render/templates/cosmos/project.liquid +327 -0
- package/dist/render/templates/cosmos/session.liquid +239 -0
- package/dist/render/templates/cosmos/styles.css +1157 -0
- package/dist/render/templates/daylight/portfolio.liquid +207 -0
- package/dist/render/templates/daylight/project.liquid +229 -0
- package/dist/render/templates/daylight/session.liquid +219 -0
- package/dist/render/templates/daylight/styles.css +1315 -0
- package/dist/render/templates/editorial/portfolio.liquid +110 -0
- package/dist/render/templates/editorial/project.liquid +202 -0
- package/dist/render/templates/editorial/session.liquid +171 -0
- package/dist/render/templates/editorial/styles.css +826 -0
- package/dist/render/templates/ember/portfolio.liquid +306 -0
- package/dist/render/templates/ember/project.liquid +232 -0
- package/dist/render/templates/ember/session.liquid +202 -0
- package/dist/render/templates/ember/styles.css +1289 -0
- package/dist/render/templates/glacier/portfolio.liquid +261 -0
- package/dist/render/templates/glacier/project.liquid +288 -0
- package/dist/render/templates/glacier/session.liquid +217 -0
- package/dist/render/templates/glacier/styles.css +1204 -0
- package/dist/render/templates/grid/portfolio.liquid +255 -0
- package/dist/render/templates/grid/project.liquid +306 -0
- package/dist/render/templates/grid/session.liquid +260 -0
- package/dist/render/templates/grid/styles.css +1445 -0
- package/dist/render/templates/kinetic/portfolio.liquid +158 -0
- package/dist/render/templates/kinetic/project.liquid +242 -0
- package/dist/render/templates/kinetic/session.liquid +228 -0
- package/dist/render/templates/kinetic/styles.css +948 -0
- package/dist/render/templates/meridian/portfolio.liquid +243 -0
- package/dist/render/templates/meridian/project.liquid +376 -0
- package/dist/render/templates/meridian/session.liquid +298 -0
- package/dist/render/templates/meridian/styles.css +1375 -0
- package/dist/render/templates/minimal/portfolio.liquid +71 -0
- package/dist/render/templates/minimal/project.liquid +154 -0
- package/dist/render/templates/minimal/session.liquid +140 -0
- package/dist/render/templates/minimal/styles.css +529 -0
- package/dist/render/templates/mono/portfolio.liquid +281 -0
- package/dist/render/templates/mono/project.liquid +275 -0
- package/dist/render/templates/mono/session.liquid +276 -0
- package/dist/render/templates/mono/styles.css +1022 -0
- package/dist/render/templates/neon/portfolio.liquid +207 -0
- package/dist/render/templates/neon/project.liquid +225 -0
- package/dist/render/templates/neon/session.liquid +195 -0
- package/dist/render/templates/neon/styles.css +1271 -0
- package/dist/render/templates/noir/portfolio.liquid +137 -0
- package/dist/render/templates/noir/project.liquid +220 -0
- package/dist/render/templates/noir/session.liquid +241 -0
- package/dist/render/templates/noir/styles.css +1229 -0
- package/dist/render/templates/obsidian/portfolio.liquid +247 -0
- package/dist/render/templates/obsidian/project.liquid +280 -0
- package/dist/render/templates/obsidian/session.liquid +241 -0
- package/dist/render/templates/obsidian/styles.css +1407 -0
- package/dist/render/templates/paper/portfolio.liquid +257 -0
- package/dist/render/templates/paper/project.liquid +235 -0
- package/dist/render/templates/paper/session.liquid +271 -0
- package/dist/render/templates/paper/styles.css +1513 -0
- package/dist/render/templates/parallax/portfolio.liquid +295 -0
- package/dist/render/templates/parallax/project.liquid +275 -0
- package/dist/render/templates/parallax/session.liquid +295 -0
- package/dist/render/templates/parallax/styles.css +1880 -0
- package/dist/render/templates/parchment/portfolio.liquid +280 -0
- package/dist/render/templates/parchment/project.liquid +289 -0
- package/dist/render/templates/parchment/session.liquid +346 -0
- package/dist/render/templates/parchment/styles.css +1401 -0
- package/dist/render/templates/partials/_beats.liquid +16 -0
- package/dist/render/templates/partials/_breadcrumb.liquid +9 -0
- package/dist/render/templates/partials/_footer.liquid +7 -0
- package/dist/render/templates/partials/_growth-chart.liquid +7 -0
- package/dist/render/templates/partials/_key-decisions.liquid +20 -0
- package/dist/render/templates/partials/_links.liquid +16 -0
- package/dist/render/templates/partials/_narrative.liquid +8 -0
- package/dist/render/templates/partials/_phases.liquid +20 -0
- package/dist/render/templates/partials/_portfolio-header.liquid +20 -0
- package/dist/render/templates/partials/_portfolio-projects.liquid +16 -0
- package/dist/render/templates/partials/_portfolio-stats.liquid +19 -0
- package/dist/render/templates/partials/_qa.liquid +13 -0
- package/dist/render/templates/partials/_screenshot.liquid +15 -0
- package/dist/render/templates/partials/_session-cards.liquid +30 -0
- package/dist/render/templates/partials/_session-header.liquid +39 -0
- package/dist/render/templates/partials/_session-sidebar.liquid +30 -0
- package/dist/render/templates/partials/_skills.liquid +12 -0
- package/dist/render/templates/partials/_source-breakdown.liquid +22 -0
- package/dist/render/templates/partials/_stats.liquid +38 -0
- package/dist/render/templates/partials/_work-timeline.liquid +7 -0
- package/dist/render/templates/project.liquid +7 -4
- package/dist/render/templates/radar/portfolio.liquid +223 -0
- package/dist/render/templates/radar/project.liquid +278 -0
- package/dist/render/templates/radar/session.liquid +300 -0
- package/dist/render/templates/radar/styles.css +1055 -0
- package/dist/render/templates/showcase/portfolio.liquid +221 -0
- package/dist/render/templates/showcase/project.liquid +237 -0
- package/dist/render/templates/showcase/session.liquid +210 -0
- package/dist/render/templates/showcase/styles.css +1284 -0
- package/dist/render/templates/signal/portfolio.liquid +217 -0
- package/dist/render/templates/signal/project.liquid +278 -0
- package/dist/render/templates/signal/session.liquid +282 -0
- package/dist/render/templates/signal/styles.css +1401 -0
- package/dist/render/templates/strata/portfolio.liquid +180 -0
- package/dist/render/templates/strata/project.liquid +282 -0
- package/dist/render/templates/strata/session.liquid +261 -0
- package/dist/render/templates/strata/styles.css +1354 -0
- package/dist/render/templates/styles.css +1190 -0
- package/dist/render/templates/terminal/portfolio.liquid +102 -0
- package/dist/render/templates/terminal/project.liquid +161 -0
- package/dist/render/templates/terminal/session.liquid +145 -0
- package/dist/render/templates/terminal/styles.css +497 -0
- package/dist/render/templates/verdant/portfolio.liquid +321 -0
- package/dist/render/templates/verdant/project.liquid +309 -0
- package/dist/render/templates/verdant/session.liquid +237 -0
- package/dist/render/templates/verdant/styles.css +1261 -0
- package/dist/render/templates/zen/portfolio.liquid +124 -0
- package/dist/render/templates/zen/project.liquid +187 -0
- package/dist/render/templates/zen/session.liquid +203 -0
- package/dist/render/templates/zen/styles.css +1211 -0
- package/dist/render/templates.js +90 -0
- package/dist/routes/auth.js +7 -3
- package/dist/routes/context.js +17 -10
- package/dist/routes/delete.js +195 -0
- package/dist/routes/enhance.js +57 -40
- package/dist/routes/export.js +14 -4
- package/dist/routes/github.js +254 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/portfolio-render-data.js +160 -0
- package/dist/routes/preview.js +555 -108
- package/dist/routes/projects.js +61 -24
- package/dist/routes/publish.js +320 -31
- package/dist/routes/settings.js +194 -1
- package/dist/routes/sse.js +9 -0
- package/dist/search.js +6 -0
- package/dist/server.js +11 -3
- package/dist/settings.js +112 -9
- package/package.json +3 -4
- package/dist/public/assets/index-CC9G8EF1.js +0 -21
- package/dist/public/assets/index-Dalqz2mC.css +0 -1
package/dist/public/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>app</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D0noVMFu.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Coilyhtr.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist/redact.js
CHANGED
|
@@ -1,74 +1,16 @@
|
|
|
1
1
|
// Redact — scans text for secrets, PII, and sensitive file paths before publish.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// 1. secretlint (community-maintained rules for Anthropic, GitHub, Slack, npm, etc.)
|
|
5
|
-
// 2. Custom regex (fills gaps: OpenAI, Stripe, Google, JWT, Bearer, PII, paths)
|
|
3
|
+
// Custom regex patterns for common secret formats (API keys, tokens, PII, paths).
|
|
6
4
|
//
|
|
7
5
|
// Two severity levels:
|
|
8
6
|
// HIGH — auto-redacted (known API key prefixes, private keys, connection strings)
|
|
9
7
|
// MEDIUM — flagged for user review (email addresses)
|
|
10
8
|
//
|
|
11
9
|
// Usage:
|
|
12
|
-
// const findings =
|
|
13
|
-
// const cleaned =
|
|
14
|
-
// const results = await scanSession(session);
|
|
10
|
+
// const findings = scanTextSync(text);
|
|
11
|
+
// const cleaned = redactText(text);
|
|
15
12
|
import { homedir } from "node:os";
|
|
16
|
-
// ── Secretlint integration ─────────────────────────────────────
|
|
17
|
-
let _engine = null;
|
|
18
|
-
let _engineInitFailed = false;
|
|
19
|
-
async function getSecretlintEngine() {
|
|
20
|
-
if (_engine)
|
|
21
|
-
return _engine;
|
|
22
|
-
if (_engineInitFailed)
|
|
23
|
-
return null;
|
|
24
|
-
try {
|
|
25
|
-
const { createEngine } = await import("@secretlint/node");
|
|
26
|
-
_engine = await createEngine({
|
|
27
|
-
color: false,
|
|
28
|
-
formatter: "json",
|
|
29
|
-
maskSecrets: false,
|
|
30
|
-
configFileJSON: {
|
|
31
|
-
rules: [{
|
|
32
|
-
id: "@secretlint/secretlint-rule-preset-recommend",
|
|
33
|
-
}],
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
return _engine;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
_engineInitFailed = true;
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function secretlintScan(text) {
|
|
44
|
-
const engine = await getSecretlintEngine();
|
|
45
|
-
if (!engine)
|
|
46
|
-
return [];
|
|
47
|
-
try {
|
|
48
|
-
const { ok, output } = await engine.executeOnContent({ content: text, filePath: "/scan.txt" });
|
|
49
|
-
if (ok)
|
|
50
|
-
return []; // no findings
|
|
51
|
-
const results = JSON.parse(output);
|
|
52
|
-
const findings = [];
|
|
53
|
-
for (const file of results) {
|
|
54
|
-
for (const msg of file.messages ?? []) {
|
|
55
|
-
findings.push({
|
|
56
|
-
pattern: msg.ruleId.replace("@secretlint/secretlint-rule-", ""),
|
|
57
|
-
severity: "high",
|
|
58
|
-
category: "secret",
|
|
59
|
-
match: msg.message.length > 60 ? msg.message.slice(0, 50) + "..." : msg.message,
|
|
60
|
-
index: msg.loc?.start?.offset ?? 0,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return findings;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
13
|
// All regex patterns — used for both scanning AND redaction (find-and-replace).
|
|
71
|
-
// secretlint adds additional detection on top; deduplication prevents double-counting.
|
|
72
14
|
const CUSTOM_SECRET_PATTERNS = [
|
|
73
15
|
// ─── AWS ───────────────────────────────────────────────
|
|
74
16
|
{
|
|
@@ -224,15 +166,7 @@ function regexScan(text) {
|
|
|
224
166
|
}
|
|
225
167
|
return findings;
|
|
226
168
|
}
|
|
227
|
-
/** Scan text for secrets and PII using
|
|
228
|
-
export async function scanText(text) {
|
|
229
|
-
const [slFindings, regexFindings] = await Promise.all([
|
|
230
|
-
secretlintScan(text),
|
|
231
|
-
Promise.resolve(regexScan(text)),
|
|
232
|
-
]);
|
|
233
|
-
return deduplicateFindings([...slFindings, ...regexFindings]);
|
|
234
|
-
}
|
|
235
|
-
/** Synchronous scan using only custom regex patterns (no secretlint). */
|
|
169
|
+
/** Scan text for secrets and PII using custom regex patterns. */
|
|
236
170
|
export function scanTextSync(text) {
|
|
237
171
|
return regexScan(text);
|
|
238
172
|
}
|
|
@@ -277,40 +211,6 @@ export function stripHomePathsInText(text, cwd) {
|
|
|
277
211
|
const homeRe = new RegExp(homePattern + "[/\\\\]?", "g");
|
|
278
212
|
return text.replace(homeRe, "~/");
|
|
279
213
|
}
|
|
280
|
-
// ── Session-level operations ───────────────────────────────────
|
|
281
|
-
/** Scan all string fields in a session object for secrets/PII. */
|
|
282
|
-
export async function scanSession(session) {
|
|
283
|
-
// Collect all string values with their paths
|
|
284
|
-
const strings = [];
|
|
285
|
-
function walk(value, path) {
|
|
286
|
-
if (typeof value === "string" && value.length > 0) {
|
|
287
|
-
strings.push({ value, path });
|
|
288
|
-
}
|
|
289
|
-
else if (Array.isArray(value)) {
|
|
290
|
-
for (let i = 0; i < value.length; i++)
|
|
291
|
-
walk(value[i], `${path}[${i}]`);
|
|
292
|
-
}
|
|
293
|
-
else if (value && typeof value === "object") {
|
|
294
|
-
for (const [k, v] of Object.entries(value))
|
|
295
|
-
walk(v, path ? `${path}.${k}` : k);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
walk(session, "");
|
|
299
|
-
// Batch all strings into one scan for secretlint efficiency
|
|
300
|
-
const allText = strings.map((s) => s.value).join("\n---FIELD_BOUNDARY---\n");
|
|
301
|
-
const allFindings = await scanText(allText);
|
|
302
|
-
// Map findings back to field paths
|
|
303
|
-
const fieldsWithFindings = [];
|
|
304
|
-
let offset = 0;
|
|
305
|
-
for (const { value, path } of strings) {
|
|
306
|
-
const endOffset = offset + value.length;
|
|
307
|
-
const fieldFindings = allFindings.filter((f) => f.index >= offset && f.index < endOffset);
|
|
308
|
-
if (fieldFindings.length > 0)
|
|
309
|
-
fieldsWithFindings.push(path);
|
|
310
|
-
offset = endOffset + "\n---FIELD_BOUNDARY---\n".length;
|
|
311
|
-
}
|
|
312
|
-
return { findings: allFindings, fieldsWithFindings };
|
|
313
|
-
}
|
|
314
214
|
/** Deep-redact all string fields + strip paths. Returns a new object. */
|
|
315
215
|
export function redactSession(session, mode = "high", cwd) {
|
|
316
216
|
return deepRedact(session, mode, cwd);
|
|
@@ -25,6 +25,7 @@ export function buildSessionRenderData(opts) {
|
|
|
25
25
|
devTake,
|
|
26
26
|
context: enhanced?.context ?? '',
|
|
27
27
|
durationMinutes: session.durationMinutes ?? 0,
|
|
28
|
+
wallClockMinutes: session.wallClockMinutes,
|
|
28
29
|
turns: session.turns ?? 0,
|
|
29
30
|
filesChanged: session.filesChanged?.length ?? 0,
|
|
30
31
|
locChanged: session.linesOfCode ?? 0,
|
|
@@ -41,7 +42,7 @@ export function buildSessionRenderData(opts) {
|
|
|
41
42
|
topFiles: (session.filesChanged ?? []).slice(0, 20).map((f) => typeof f === 'string' ? { path: f, additions: 0, deletions: 0 } : f),
|
|
42
43
|
recordedAt: sessionRecordedAt,
|
|
43
44
|
sourceTool,
|
|
44
|
-
template: 'editorial',
|
|
45
|
+
template: opts.template ?? 'editorial',
|
|
45
46
|
agentSummary: agentSummary ?? undefined,
|
|
46
47
|
},
|
|
47
48
|
};
|
|
@@ -56,15 +57,21 @@ export function buildSessionCard(opts) {
|
|
|
56
57
|
const sessionTitle = enhanced?.title ?? session.title;
|
|
57
58
|
const sessionSkills = enhanced?.skills ?? session.skills ?? [];
|
|
58
59
|
const sessionRecordedAt = session.date ? new Date(session.date).toISOString() : new Date().toISOString();
|
|
60
|
+
const files = session.filesChanged ?? [];
|
|
61
|
+
const totalAdded = files.reduce((sum, f) => sum + ((typeof f === 'string' ? 0 : f.additions) ?? 0), 0);
|
|
62
|
+
const totalDeleted = files.reduce((sum, f) => sum + ((typeof f === 'string' ? 0 : f.deletions) ?? 0), 0);
|
|
59
63
|
return {
|
|
60
64
|
token: sessionId,
|
|
61
65
|
slug: sessionSlug,
|
|
62
66
|
title: sessionTitle,
|
|
63
67
|
devTake,
|
|
64
68
|
durationMinutes: session.durationMinutes ?? 0,
|
|
69
|
+
wallClockMinutes: session.wallClockMinutes,
|
|
65
70
|
turns: session.turns ?? 0,
|
|
66
71
|
locChanged: session.linesOfCode ?? 0,
|
|
67
|
-
|
|
72
|
+
linesAdded: totalAdded,
|
|
73
|
+
linesDeleted: totalDeleted,
|
|
74
|
+
filesChanged: files.length,
|
|
68
75
|
skills: sessionSkills,
|
|
69
76
|
recordedAt: sessionRecordedAt,
|
|
70
77
|
sourceTool,
|
package/dist/render/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* These functions run on the Node.js server, never in the browser.
|
|
9
9
|
*/
|
|
10
|
-
import { renderProject, renderSession } from './liquid.js';
|
|
10
|
+
import { renderPortfolio, renderProject, renderSession } from './liquid.js';
|
|
11
11
|
/** Errors from the render pipeline carry a machine-readable code. */
|
|
12
12
|
export class RenderError extends Error {
|
|
13
13
|
code;
|
|
@@ -77,25 +77,52 @@ function validateSession(data) {
|
|
|
77
77
|
* @throws {RenderError} with code VALIDATION_ERROR if required fields are missing
|
|
78
78
|
* @throws {RenderError} with code RENDER_FAILED if Liquid rendering fails
|
|
79
79
|
*/
|
|
80
|
-
export function renderProjectHtml(data, extras) {
|
|
80
|
+
export function renderProjectHtml(data, extras, templateName) {
|
|
81
81
|
validateProject(data);
|
|
82
82
|
try {
|
|
83
|
-
return renderProject(data, extras);
|
|
83
|
+
return renderProject(data, extras, templateName);
|
|
84
84
|
}
|
|
85
85
|
catch (err) {
|
|
86
86
|
throw new RenderError('RENDER_FAILED', `Failed to render project page for ${data.project.slug}`, err);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Render a portfolio page to a static HTML fragment.
|
|
91
|
+
*
|
|
92
|
+
* @throws {RenderError} with code VALIDATION_ERROR if required fields are missing
|
|
93
|
+
* @throws {RenderError} with code RENDER_FAILED if Liquid rendering fails
|
|
94
|
+
*/
|
|
95
|
+
export function renderPortfolioHtml(data, templateName) {
|
|
96
|
+
const errors = [];
|
|
97
|
+
if (!data.user) {
|
|
98
|
+
errors.push({ field: 'user', message: 'required' });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (!data.user.username)
|
|
102
|
+
errors.push({ field: 'user.username', message: 'required' });
|
|
103
|
+
}
|
|
104
|
+
if (!Array.isArray(data.projects)) {
|
|
105
|
+
errors.push({ field: 'projects', message: 'must be an array' });
|
|
106
|
+
}
|
|
107
|
+
if (errors.length > 0)
|
|
108
|
+
collectErrors(errors);
|
|
109
|
+
try {
|
|
110
|
+
return renderPortfolio(data, templateName);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
throw new RenderError('RENDER_FAILED', `Failed to render portfolio page for ${data.user?.username}`, err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
89
116
|
/**
|
|
90
117
|
* Render a session page to a static HTML fragment.
|
|
91
118
|
*
|
|
92
119
|
* @throws {RenderError} with code VALIDATION_ERROR if required fields are missing
|
|
93
120
|
* @throws {RenderError} with code RENDER_FAILED if Liquid rendering fails
|
|
94
121
|
*/
|
|
95
|
-
export function renderSessionHtml(data) {
|
|
122
|
+
export function renderSessionHtml(data, templateName) {
|
|
96
123
|
validateSession(data);
|
|
97
124
|
try {
|
|
98
|
-
return renderSession(data);
|
|
125
|
+
return renderSession(data, templateName);
|
|
99
126
|
}
|
|
100
127
|
catch (err) {
|
|
101
128
|
throw new RenderError('RENDER_FAILED', `Failed to render session page for ${data.session.token}`, err);
|
package/dist/render/liquid.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { Liquid } from 'liquidjs';
|
|
8
8
|
import { dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { getTemplateInfo } from './templates.js';
|
|
11
|
+
import { escapeHtml } from '../format-utils.js';
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const engine = new Liquid({
|
|
12
14
|
root: resolve(__dirname, 'templates'),
|
|
@@ -56,7 +58,7 @@ engine.registerFilter('formatDateShort', (iso) => {
|
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
60
|
engine.registerFilter('jsonAttr', (value) => {
|
|
59
|
-
return JSON.stringify(value);
|
|
61
|
+
return escapeHtml(JSON.stringify(value));
|
|
60
62
|
});
|
|
61
63
|
engine.registerFilter('localeNumber', (value) => {
|
|
62
64
|
return value.toLocaleString();
|
|
@@ -69,11 +71,20 @@ const DURATION_COLORS = ['primary', 'green', 'violet'];
|
|
|
69
71
|
engine.registerFilter('durationColor', (index) => {
|
|
70
72
|
return DURATION_COLORS[index % DURATION_COLORS.length];
|
|
71
73
|
});
|
|
72
|
-
|
|
74
|
+
const DEFAULT_TEMPLATE = 'editorial';
|
|
75
|
+
/** Inject data-accent and data-mode into the first data-template wrapper element. */
|
|
76
|
+
function injectTemplateAttrs(html, templateName) {
|
|
77
|
+
const info = getTemplateInfo(templateName);
|
|
78
|
+
if (!info)
|
|
79
|
+
return html;
|
|
80
|
+
return html.replace(/data-template="[^"]*"/, (match) => `${match} data-accent="${escapeHtml(info.accent)}" data-mode="${info.mode}"`);
|
|
81
|
+
}
|
|
82
|
+
export function renderProject(data, extras, templateName) {
|
|
83
|
+
const template = templateName || DEFAULT_TEMPLATE;
|
|
73
84
|
// Pre-compute derived data for the template
|
|
74
85
|
const allSessions = data.allSessions || data.sessions;
|
|
75
86
|
const sourceCounts = {};
|
|
76
|
-
for (const s of
|
|
87
|
+
for (const s of allSessions) {
|
|
77
88
|
const src = s.sourceTool || 'unknown';
|
|
78
89
|
sourceCounts[src] = (sourceCounts[src] || 0) + 1;
|
|
79
90
|
}
|
|
@@ -82,7 +93,8 @@ export function renderProject(data, extras) {
|
|
|
82
93
|
const chartSessions = (extras?.fullSessions ?? allSessions.map((s) => ({
|
|
83
94
|
id: s.token, slug: s.slug, title: s.title, date: s.recordedAt,
|
|
84
95
|
durationMinutes: s.durationMinutes, turns: s.turns,
|
|
85
|
-
linesOfCode: s.locChanged,
|
|
96
|
+
linesOfCode: s.locChanged, linesAdded: s.linesAdded, linesDeleted: s.linesDeleted,
|
|
97
|
+
status: 'enhanced',
|
|
86
98
|
projectName: data.project.title, rawLog: [],
|
|
87
99
|
skills: s.skills, source: s.sourceTool,
|
|
88
100
|
filesChanged: s.filesChanged,
|
|
@@ -93,6 +105,45 @@ export function renderProject(data, extras) {
|
|
|
93
105
|
// Encode JSON safe for single-quoted HTML attributes
|
|
94
106
|
const sessionsJson = JSON.stringify(chartSessions).replace(/'/g, ''');
|
|
95
107
|
const growthJson = sessionsJson; // same data for both charts
|
|
108
|
+
// Pre-compute chart coordinates for Liquid-rendered SVGs
|
|
109
|
+
// Sort all sessions by date, compress gaps, compute x positions
|
|
110
|
+
const sortedAll = [...(data.allSessions || data.sessions)]
|
|
111
|
+
.filter(s => s.recordedAt)
|
|
112
|
+
.sort((a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime());
|
|
113
|
+
// Gap compression — must match GrowthChart.tsx constants
|
|
114
|
+
const GAP_THRESHOLD = 60 * 60 * 1000; // 1 hour
|
|
115
|
+
const COMPRESSED_GAP = 10 * 60 * 1000; // 10 minutes visual
|
|
116
|
+
const visualTimes = [0];
|
|
117
|
+
for (let i = 1; i < sortedAll.length; i++) {
|
|
118
|
+
const gap = new Date(sortedAll[i].recordedAt).getTime() - new Date(sortedAll[i - 1].recordedAt).getTime();
|
|
119
|
+
visualTimes.push(visualTimes[i - 1] + (gap > GAP_THRESHOLD ? COMPRESSED_GAP : Math.max(gap, 0)));
|
|
120
|
+
}
|
|
121
|
+
const totalVisualTime = visualTimes[visualTimes.length - 1] || 1;
|
|
122
|
+
// Compute x positions (0-1000 range, template will scale to SVG width)
|
|
123
|
+
// Also compute cumulative additions/deletions for growth chart
|
|
124
|
+
let cumAdded = 0;
|
|
125
|
+
let cumDeleted = 0;
|
|
126
|
+
const chartPoints = sortedAll.map((s, i) => {
|
|
127
|
+
cumAdded += s.linesAdded || 0;
|
|
128
|
+
cumDeleted += s.linesDeleted || 0;
|
|
129
|
+
return {
|
|
130
|
+
title: s.title,
|
|
131
|
+
slug: s.slug,
|
|
132
|
+
date: s.recordedAt,
|
|
133
|
+
locChanged: s.locChanged,
|
|
134
|
+
linesAdded: s.linesAdded || 0,
|
|
135
|
+
linesDeleted: s.linesDeleted || 0,
|
|
136
|
+
durationMinutes: s.durationMinutes,
|
|
137
|
+
sourceTool: s.sourceTool || 'unknown',
|
|
138
|
+
cumAdded,
|
|
139
|
+
cumDeleted,
|
|
140
|
+
// x position as integer 0-1000 (template divides by 1000 and multiplies by plot width)
|
|
141
|
+
xPct: Math.round((visualTimes[i] / totalVisualTime) * 1000),
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
// SVG width hint: wider for more sessions (min 1200, scale with count)
|
|
145
|
+
// Match React GrowthChart sizing: base on compressed time, not raw session count
|
|
146
|
+
const chartSvgWidth = Math.max(700, Math.round(totalVisualTime / 60000 * 0.8) + 120);
|
|
96
147
|
const durationLabel = data.project.totalAgentDurationMinutes ? 'Human / Agents' : 'Time';
|
|
97
148
|
const efficiencyMultiplier = data.project.totalAgentDurationMinutes && data.project.totalDurationMinutes > 0
|
|
98
149
|
? (data.project.totalAgentDurationMinutes / data.project.totalDurationMinutes)
|
|
@@ -142,17 +193,106 @@ export function renderProject(data, extras) {
|
|
|
142
193
|
seen.add(s.token);
|
|
143
194
|
return true;
|
|
144
195
|
}).slice(0, 6);
|
|
145
|
-
|
|
196
|
+
const sessionSuffix = data.sessionBaseUrl ? '.html' : '';
|
|
197
|
+
const html = engine.renderFileSync(`${template}/project`, {
|
|
146
198
|
...data,
|
|
147
199
|
arc: extras?.arc ?? [],
|
|
148
200
|
featuredSessions,
|
|
201
|
+
chartPoints,
|
|
202
|
+
chartSvgWidth,
|
|
149
203
|
sourceCounts: Object.entries(sourceCounts).map(([tool, count]) => ({ tool, count })),
|
|
150
204
|
sessionsJson,
|
|
151
205
|
growthJson,
|
|
152
206
|
durationLabel,
|
|
153
207
|
efficiencyMultiplier: efficiencyStr,
|
|
208
|
+
sessionSuffix,
|
|
154
209
|
});
|
|
210
|
+
return injectTemplateAttrs(html, template);
|
|
155
211
|
}
|
|
156
|
-
export function renderSession(data) {
|
|
157
|
-
|
|
212
|
+
export function renderSession(data, templateName) {
|
|
213
|
+
const template = templateName || DEFAULT_TEMPLATE;
|
|
214
|
+
return injectTemplateAttrs(engine.renderFileSync(`${template}/session`, data), template);
|
|
215
|
+
}
|
|
216
|
+
export function renderPortfolio(data, templateName) {
|
|
217
|
+
const template = templateName || DEFAULT_TEMPLATE;
|
|
218
|
+
const durationLabel = data.totalAgentDurationMinutes ? 'Human / Agents' : 'Time';
|
|
219
|
+
const efficiencyMultiplier = data.totalAgentDurationMinutes && data.totalDurationMinutes > 0
|
|
220
|
+
? (data.totalAgentDurationMinutes / data.totalDurationMinutes)
|
|
221
|
+
: undefined;
|
|
222
|
+
const efficiencyStr = efficiencyMultiplier && efficiencyMultiplier > 1 ? `${efficiencyMultiplier.toFixed(1)}x` : undefined;
|
|
223
|
+
const u = data.user;
|
|
224
|
+
const hasProfile = !!(u.displayName || u.bio || u.photoUrl || u.location || u.email || u.phone || u.linkedinUrl || u.githubUrl || u.twitterHandle || u.websiteUrl || u.resumeUrl);
|
|
225
|
+
// Aggregate skills across all projects with counts
|
|
226
|
+
const skillMap = new Map();
|
|
227
|
+
for (const p of data.projects) {
|
|
228
|
+
for (const s of p.skills) {
|
|
229
|
+
skillMap.set(s, (skillMap.get(s) || 0) + 1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const allSkills = [...skillMap.entries()]
|
|
233
|
+
.map(([name, projectCount]) => ({ name, projectCount }))
|
|
234
|
+
.sort((a, b) => b.projectCount - a.projectCount);
|
|
235
|
+
const topSkills = allSkills.slice(0, 8);
|
|
236
|
+
// Aggregate source counts across all projects
|
|
237
|
+
const sourceMap = new Map();
|
|
238
|
+
for (const p of data.projects) {
|
|
239
|
+
for (const sc of p.sourceCounts || []) {
|
|
240
|
+
sourceMap.set(sc.tool, (sourceMap.get(sc.tool) || 0) + sc.count);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const sourceCounts = [...sourceMap.entries()]
|
|
244
|
+
.map(([tool, count]) => ({ tool, count }))
|
|
245
|
+
.sort((a, b) => b.count - a.count);
|
|
246
|
+
const totalSourceSessions = sourceCounts.reduce((sum, s) => sum + s.count, 0);
|
|
247
|
+
const activityByDay = computeActivityByDay(data.projects);
|
|
248
|
+
const activityByMonth = computeActivityByMonth(data.projects);
|
|
249
|
+
return injectTemplateAttrs(engine.renderFileSync(`${template}/portfolio`, {
|
|
250
|
+
...data,
|
|
251
|
+
durationLabel,
|
|
252
|
+
efficiencyMultiplier: efficiencyStr,
|
|
253
|
+
hasProfile,
|
|
254
|
+
allSkills,
|
|
255
|
+
topSkills,
|
|
256
|
+
sourceCounts,
|
|
257
|
+
totalSourceSessions,
|
|
258
|
+
activityByDay,
|
|
259
|
+
activityByMonth,
|
|
260
|
+
}), template);
|
|
261
|
+
}
|
|
262
|
+
export function computeActivityByDay(projects) {
|
|
263
|
+
const dayMap = new Map();
|
|
264
|
+
for (const p of projects) {
|
|
265
|
+
for (const s of p.sessions || []) {
|
|
266
|
+
const day = s.date.slice(0, 10);
|
|
267
|
+
const existing = dayMap.get(day) || { count: 0, loc: 0 };
|
|
268
|
+
dayMap.set(day, { count: existing.count + 1, loc: existing.loc + s.loc });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return [...dayMap.entries()]
|
|
272
|
+
.map(([date, d]) => ({ date, ...d }))
|
|
273
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
274
|
+
}
|
|
275
|
+
export function computeActivityByMonth(projects) {
|
|
276
|
+
const monthMap = new Map();
|
|
277
|
+
for (const p of projects) {
|
|
278
|
+
for (const s of p.sessions || []) {
|
|
279
|
+
const monthKey = s.date.slice(0, 7);
|
|
280
|
+
const existing = monthMap.get(monthKey) || { sessions: 0, loc: 0, projects: new Map() };
|
|
281
|
+
existing.sessions++;
|
|
282
|
+
existing.loc += s.loc;
|
|
283
|
+
const projData = existing.projects.get(p.title) || { sessions: 0, loc: 0 };
|
|
284
|
+
projData.sessions++;
|
|
285
|
+
projData.loc += s.loc;
|
|
286
|
+
existing.projects.set(p.title, projData);
|
|
287
|
+
monthMap.set(monthKey, existing);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return [...monthMap.entries()]
|
|
291
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
292
|
+
.map(([key, d]) => ({
|
|
293
|
+
month: new Date(key + '-01').toLocaleDateString('en-US', { month: 'short' }),
|
|
294
|
+
sessions: d.sessions,
|
|
295
|
+
loc: d.loc,
|
|
296
|
+
projects: [...d.projects.entries()].map(([name, pd]) => ({ name, ...pd })),
|
|
297
|
+
}));
|
|
158
298
|
}
|