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/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# heyiam
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/heyiam)
|
|
4
|
+
[](https://github.com/interactivecats/heyi.am/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Local-first CLI that indexes your AI coding sessions. Search locally, enhance with AI, and publish a portfolio of real work.
|
|
7
|
+
|
|
8
|
+
Discovers sessions from **Claude Code**, **Cursor**, **OpenAI Codex CLI**, and **Google Gemini CLI**.
|
|
9
|
+
|
|
10
|
+
## Get Started
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx heyiam
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Opens a local dashboard at `localhost:17845`. Browse projects, search sessions, and publish portfolio case studies to `heyi.am/:username`.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
| Command | Description |
|
|
21
|
+
|---------|-------------|
|
|
22
|
+
| `heyiam` / `heyiam open` | Start local dashboard |
|
|
23
|
+
| `heyiam search [query]` | Full-text search across all sessions |
|
|
24
|
+
| `heyiam time` | Your time vs agent time per project |
|
|
25
|
+
| `heyiam context <id>` | Export session as compressed context for AI tools |
|
|
26
|
+
| `heyiam archive` | Discover and archive sessions from all sources |
|
|
27
|
+
| `heyiam sync` | Index sessions into SQLite |
|
|
28
|
+
| `heyiam status` | Archive health, session counts, daemon status |
|
|
29
|
+
|
|
30
|
+
## Privacy
|
|
31
|
+
|
|
32
|
+
Everything stays local by default. Nothing leaves your machine unless you explicitly publish.
|
|
33
|
+
|
|
34
|
+
- Sessions are read from local tool storage (read-only)
|
|
35
|
+
- SQLite search index lives at `~/.local/share/heyiam/`
|
|
36
|
+
- Config at `~/.config/heyiam/`
|
|
37
|
+
- Common secret patterns are detected and redacted before upload, but this is not a guarantee
|
|
38
|
+
|
|
39
|
+
AI enhancement runs locally using your own `ANTHROPIC_API_KEY`.
|
|
40
|
+
|
|
41
|
+
## Links
|
|
42
|
+
|
|
43
|
+
- [heyi.am](https://heyi.am) — published portfolios
|
|
44
|
+
- [heyiam.com](https://heyiam.com) — dashboard and auth
|
|
45
|
+
- [GitHub](https://github.com/interactivecats/heyi.am) — source code
|
package/dist/auth.js
CHANGED
|
@@ -19,7 +19,16 @@ export function writeConfig(filename, data, configDir = getConfigDir()) {
|
|
|
19
19
|
writeFileSync(join(configDir, filename), JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
20
20
|
}
|
|
21
21
|
export function getAuthToken(configDir = getConfigDir()) {
|
|
22
|
-
|
|
22
|
+
const raw = readConfig(AUTH_FILE, configDir);
|
|
23
|
+
if (!raw)
|
|
24
|
+
return null;
|
|
25
|
+
// Legacy configs may contain mixed-case usernames (prior to the
|
|
26
|
+
// normalize-on-write change). Always project to the canonical form so
|
|
27
|
+
// callers — URL construction, display, server requests — stay consistent.
|
|
28
|
+
if (raw.username && raw.username !== raw.username.toLowerCase()) {
|
|
29
|
+
return { ...raw, username: normalizeUsername(raw.username) };
|
|
30
|
+
}
|
|
31
|
+
return raw;
|
|
23
32
|
}
|
|
24
33
|
export function deleteAuthToken(configDir = getConfigDir()) {
|
|
25
34
|
const filePath = join(configDir, AUTH_FILE);
|
|
@@ -27,8 +36,22 @@ export function deleteAuthToken(configDir = getConfigDir()) {
|
|
|
27
36
|
unlinkSync(filePath);
|
|
28
37
|
}
|
|
29
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a username to the canonical form used everywhere in the CLI:
|
|
41
|
+
* lowercase, whitespace trimmed. Mirrors Phoenix's DB validation regex
|
|
42
|
+
* `^[a-z0-9-]+$`. Guards against Phoenix responses or legacy configs that
|
|
43
|
+
* contain mixed-case usernames (e.g. "Ben" -> "ben") so URL construction
|
|
44
|
+
* and display are consistent.
|
|
45
|
+
*/
|
|
46
|
+
export function normalizeUsername(username) {
|
|
47
|
+
return username.trim().toLowerCase();
|
|
48
|
+
}
|
|
30
49
|
export function saveAuthToken(token, username, configDir = getConfigDir()) {
|
|
31
|
-
const config = {
|
|
50
|
+
const config = {
|
|
51
|
+
token,
|
|
52
|
+
username: normalizeUsername(username),
|
|
53
|
+
savedAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
32
55
|
writeConfig(AUTH_FILE, config, configDir);
|
|
33
56
|
}
|
|
34
57
|
export async function checkAuthStatus(apiBaseUrl, configDir = getConfigDir(), fetchFn = fetch) {
|
|
@@ -41,7 +64,10 @@ export async function checkAuthStatus(apiBaseUrl, configDir = getConfigDir(), fe
|
|
|
41
64
|
if (!res.ok)
|
|
42
65
|
return { authenticated: false };
|
|
43
66
|
const body = (await res.json());
|
|
44
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
authenticated: true,
|
|
69
|
+
username: body.username ? normalizeUsername(body.username) : body.username,
|
|
70
|
+
};
|
|
45
71
|
}
|
|
46
72
|
export async function deviceAuthFlow(apiBaseUrl, configDir = getConfigDir(), options = {}) {
|
|
47
73
|
const fetchFn = options.fetchFn ?? fetch;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
/** Base URL for the heyiam.com app API. Override with HEYIAM_API_URL for local dev. */
|
|
2
|
-
|
|
2
|
+
const DEFAULT_API_URL = 'https://heyiam.com';
|
|
3
|
+
export const API_URL = process.env.HEYIAM_API_URL ?? DEFAULT_API_URL;
|
|
3
4
|
/** Base URL for the heyi.am public site. Override with HEYIAM_PUBLIC_URL for local dev. */
|
|
4
5
|
export const PUBLIC_URL = process.env.HEYIAM_PUBLIC_URL ?? 'https://heyi.am';
|
|
6
|
+
/** Warn once to stderr if a non-default API URL is in use (env var override). */
|
|
7
|
+
let _apiUrlWarned = false;
|
|
8
|
+
export function warnIfNonDefaultApiUrl() {
|
|
9
|
+
if (!_apiUrlWarned && API_URL !== DEFAULT_API_URL) {
|
|
10
|
+
console.warn(`[security] API_URL overridden to ${API_URL} — auth tokens will be sent to this host`);
|
|
11
|
+
_apiUrlWarned = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -12,7 +12,6 @@ function getDataDir() {
|
|
|
12
12
|
export function getDbPath() {
|
|
13
13
|
return join(getDataDir(), 'sessions.db');
|
|
14
14
|
}
|
|
15
|
-
export const DB_PATH = join(homedir(), '.local', 'share', 'heyiam', 'sessions.db');
|
|
16
15
|
const CURRENT_SCHEMA_VERSION = 5;
|
|
17
16
|
// ── Singleton ────────────────────────────────────────────────
|
|
18
17
|
let _db = null;
|
package/dist/export.js
CHANGED
|
@@ -9,9 +9,10 @@ import { mkdirSync, writeFileSync, readFileSync, statSync, existsSync } from 'no
|
|
|
9
9
|
import { deflateRawSync } from 'node:zlib';
|
|
10
10
|
import { join, resolve, dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { loadEnhancedData } from './settings.js';
|
|
13
|
-
import { renderProjectHtml, renderSessionHtml } from './render/index.js';
|
|
14
|
-
import {
|
|
12
|
+
import { loadEnhancedData, getDefaultTemplate } from './settings.js';
|
|
13
|
+
import { renderProjectHtml, renderSessionHtml, renderPortfolioHtml } from './render/index.js';
|
|
14
|
+
import { resolveTemplate, getTemplateCss } from './render/templates.js';
|
|
15
|
+
import { escapeHtml, displayNameFromDir, toSlug } from './format-utils.js';
|
|
15
16
|
import { buildProjectRenderData, buildSessionRenderData, buildSessionCard, } from './render/build-render-data.js';
|
|
16
17
|
import { mergeActiveIntervals, sumIntervalMs } from './bridge.js';
|
|
17
18
|
import { SCREENSHOTS_DIR } from './screenshot.js';
|
|
@@ -42,7 +43,7 @@ function resolveScreenshotDataUri(dirName, cache) {
|
|
|
42
43
|
return `data:image/png;base64,${b64}`;
|
|
43
44
|
}
|
|
44
45
|
// Try local screenshot file
|
|
45
|
-
const slug = dirName
|
|
46
|
+
const slug = toSlug(dirName);
|
|
46
47
|
const screenshotPath = join(SCREENSHOTS_DIR, `${slug}.png`);
|
|
47
48
|
if (existsSync(screenshotPath)) {
|
|
48
49
|
const buf = readFileSync(screenshotPath);
|
|
@@ -220,16 +221,19 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
|
|
|
220
221
|
sourceTool: session.source ?? 'unknown',
|
|
221
222
|
});
|
|
222
223
|
});
|
|
223
|
-
//
|
|
224
|
+
// Prefer DB-computed stats from opts (includes subagent data); fall back to session-based computation
|
|
224
225
|
const totalLoc = sessions.reduce((sum, s) => sum + s.linesOfCode, 0);
|
|
225
226
|
const totalDurationMinutes = computeMergedSessionDuration(sessions);
|
|
226
227
|
const totalFilesChanged = opts?.totalFilesChanged ?? new Set(sessions.flatMap(s => s.filesChanged.map(f => f.path))).size;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
const totalAgentDurationMinutes = opts?.totalAgentDurationMinutes ?? (() => {
|
|
229
|
+
const agentMin = sessions
|
|
230
|
+
.filter((s) => s.isOrchestrated && s.children)
|
|
231
|
+
.reduce((sum, s) => sum + s.children.reduce((cs, c) => cs + c.durationMinutes, 0), 0);
|
|
232
|
+
return agentMin > 0 ? totalDurationMinutes + agentMin : undefined;
|
|
233
|
+
})();
|
|
234
|
+
const totalTokens = (opts?.totalInputTokens || opts?.totalOutputTokens)
|
|
235
|
+
? (opts.totalInputTokens ?? 0) + (opts.totalOutputTokens ?? 0) || undefined
|
|
236
|
+
: sessions.reduce((sum, s) => sum + (s.tokenUsage?.input ?? 0) + (s.tokenUsage?.output ?? 0), 0) || undefined;
|
|
233
237
|
// Resolve screenshot for embedding
|
|
234
238
|
const screenshotUrl = resolveScreenshotDataUri(dirName, cache);
|
|
235
239
|
// Render project index.html — pass same data shape as dashboard
|
|
@@ -256,12 +260,14 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
|
|
|
256
260
|
sessionCards,
|
|
257
261
|
sessionBaseUrl: './sessions',
|
|
258
262
|
});
|
|
263
|
+
const templateName = resolveTemplate(undefined, getDefaultTemplate());
|
|
259
264
|
const projectBody = renderProjectHtml(projectRenderData, {
|
|
260
265
|
arc: result.arc,
|
|
261
266
|
fullSessions: sessions,
|
|
262
|
-
});
|
|
267
|
+
}, templateName);
|
|
263
268
|
const projectHtml = buildStandalonePage(title, projectBody, {
|
|
264
269
|
description: result.narrative?.slice(0, 200) || undefined,
|
|
270
|
+
templateName,
|
|
265
271
|
});
|
|
266
272
|
totalBytes += writeAndTrack(join(outputPath, 'index.html'), projectHtml, files);
|
|
267
273
|
// Render session pages — only featured sessions (linked from project page)
|
|
@@ -279,10 +285,11 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
|
|
|
279
285
|
projectSlug: slug,
|
|
280
286
|
sessionSlug,
|
|
281
287
|
sourceTool: session.source ?? 'unknown',
|
|
288
|
+
template: templateName,
|
|
282
289
|
});
|
|
283
|
-
const sessionBody = renderSessionHtml(renderData);
|
|
290
|
+
const sessionBody = renderSessionHtml(renderData, templateName);
|
|
284
291
|
const sessionDesc = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 200) || undefined;
|
|
285
|
-
const sessionHtml = buildStandalonePage(session.title, sessionBody, { description: sessionDesc });
|
|
292
|
+
const sessionHtml = buildStandalonePage(session.title, sessionBody, { description: sessionDesc, templateName });
|
|
286
293
|
totalBytes += writeAndTrack(join(sessionsDir, `${sessionSlug}.html`), sessionHtml, files);
|
|
287
294
|
}
|
|
288
295
|
return { files, totalBytes, outputPath };
|
|
@@ -318,11 +325,15 @@ function buildProjectRenderInputs(dirName, cache, sessions, username, opts) {
|
|
|
318
325
|
const totalLoc = sessions.reduce((sum, s) => sum + s.linesOfCode, 0);
|
|
319
326
|
const totalDurationMinutes = computeMergedSessionDuration(sessions);
|
|
320
327
|
const totalFilesChanged = opts?.totalFilesChanged ?? new Set(sessions.flatMap(s => s.filesChanged.map(f => f.path))).size;
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
328
|
+
const totalAgentDurationMinutes = opts?.totalAgentDurationMinutes ?? (() => {
|
|
329
|
+
const agentMin = sessions
|
|
330
|
+
.filter((s) => s.isOrchestrated && s.children)
|
|
331
|
+
.reduce((sum, s) => sum + s.children.reduce((cs, c) => cs + c.durationMinutes, 0), 0);
|
|
332
|
+
return agentMin > 0 ? totalDurationMinutes + agentMin : undefined;
|
|
333
|
+
})();
|
|
334
|
+
const totalTokens = (opts?.totalInputTokens || opts?.totalOutputTokens)
|
|
335
|
+
? (opts.totalInputTokens ?? 0) + (opts.totalOutputTokens ?? 0) || undefined
|
|
336
|
+
: sessions.reduce((sum, s) => sum + (s.tokenUsage?.input ?? 0) + (s.tokenUsage?.output ?? 0), 0) || undefined;
|
|
326
337
|
return { result, slug, title, sessionCards, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens };
|
|
327
338
|
}
|
|
328
339
|
/**
|
|
@@ -346,15 +357,17 @@ export function generateProjectHtmlFragment(dirName, cache, sessions, username =
|
|
|
346
357
|
totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens,
|
|
347
358
|
sessionCards,
|
|
348
359
|
});
|
|
360
|
+
const templateName = resolveTemplate(undefined, getDefaultTemplate());
|
|
349
361
|
return renderProjectHtml(renderData, {
|
|
350
362
|
arc: result.arc,
|
|
351
363
|
fullSessions: sessions,
|
|
352
|
-
});
|
|
364
|
+
}, templateName);
|
|
353
365
|
}
|
|
354
366
|
export function generateHtmlFiles(dirName, cache, sessions, username = 'local', opts) {
|
|
355
367
|
const files = [];
|
|
356
368
|
const { result, slug, title, sessionCards, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens } = buildProjectRenderInputs(dirName, cache, sessions, username, opts);
|
|
357
369
|
const screenshotUrl = resolveScreenshotDataUri(dirName, cache);
|
|
370
|
+
const templateName = resolveTemplate(undefined, getDefaultTemplate());
|
|
358
371
|
const projectRenderData = buildProjectRenderData({
|
|
359
372
|
username, slug, title,
|
|
360
373
|
narrative: result.narrative,
|
|
@@ -371,9 +384,10 @@ export function generateHtmlFiles(dirName, cache, sessions, username = 'local',
|
|
|
371
384
|
const projectBody = renderProjectHtml(projectRenderData, {
|
|
372
385
|
arc: result.arc,
|
|
373
386
|
fullSessions: sessions,
|
|
374
|
-
});
|
|
387
|
+
}, templateName);
|
|
375
388
|
files.push({ path: 'index.html', content: buildStandalonePage(title, projectBody, {
|
|
376
389
|
description: result.narrative?.slice(0, 200) || undefined,
|
|
390
|
+
templateName,
|
|
377
391
|
}) });
|
|
378
392
|
const featuredSessions = pickFeaturedSessions(sessions, cache);
|
|
379
393
|
for (const session of featuredSessions) {
|
|
@@ -384,12 +398,13 @@ export function generateHtmlFiles(dirName, cache, sessions, username = 'local',
|
|
|
384
398
|
session, enhanced, username,
|
|
385
399
|
projectSlug: slug, sessionSlug,
|
|
386
400
|
sourceTool: session.source ?? 'unknown',
|
|
401
|
+
template: templateName,
|
|
387
402
|
});
|
|
388
|
-
const sessionBody = renderSessionHtml(renderData);
|
|
403
|
+
const sessionBody = renderSessionHtml(renderData, templateName);
|
|
389
404
|
const sessionDesc = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 200) || undefined;
|
|
390
405
|
files.push({
|
|
391
406
|
path: `sessions/${sessionSlug}.html`,
|
|
392
|
-
content: buildStandalonePage(session.title, sessionBody, { description: sessionDesc }),
|
|
407
|
+
content: buildStandalonePage(session.title, sessionBody, { description: sessionDesc, templateName }),
|
|
393
408
|
});
|
|
394
409
|
}
|
|
395
410
|
return files;
|
|
@@ -409,7 +424,9 @@ export function createZipBuffer(entries) {
|
|
|
409
424
|
const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
|
|
410
425
|
for (const entry of entries) {
|
|
411
426
|
const nameBytes = Buffer.from(entry.path, 'utf-8');
|
|
412
|
-
const raw = Buffer.
|
|
427
|
+
const raw = Buffer.isBuffer(entry.content)
|
|
428
|
+
? entry.content
|
|
429
|
+
: Buffer.from(entry.content, 'utf-8');
|
|
413
430
|
const compressed = deflateRawSync(raw);
|
|
414
431
|
const crc = crc32(raw);
|
|
415
432
|
// Local file header
|
|
@@ -490,9 +507,9 @@ function getInlineMountJs() {
|
|
|
490
507
|
}
|
|
491
508
|
}
|
|
492
509
|
function buildStandalonePage(title, bodyHtml, opts) {
|
|
493
|
-
const css = getInlineCss();
|
|
510
|
+
const css = opts?.templateName ? getTemplateCss(opts.templateName) : getInlineCss();
|
|
494
511
|
const cssTag = css
|
|
495
|
-
? `<style>${css}\nbody { overflow: auto !important; min-height: auto !important;
|
|
512
|
+
? `<style>${css}\nbody { overflow: auto !important; min-height: auto !important; }\n#root { min-height: auto !important; }</style>`
|
|
496
513
|
: '';
|
|
497
514
|
const mountJs = getInlineMountJs();
|
|
498
515
|
const scriptTag = mountJs ? `<script>${mountJs}</script>` : '';
|
|
@@ -516,7 +533,7 @@ function buildStandalonePage(title, bodyHtml, opts) {
|
|
|
516
533
|
${ogTags}
|
|
517
534
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
518
535
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
519
|
-
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
536
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Newsreader:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
|
|
520
537
|
${cssTag}
|
|
521
538
|
</head>
|
|
522
539
|
<body>
|
|
@@ -525,3 +542,83 @@ function buildStandalonePage(title, bodyHtml, opts) {
|
|
|
525
542
|
</body>
|
|
526
543
|
</html>`;
|
|
527
544
|
}
|
|
545
|
+
// ── Portfolio site export (Phase 1) ────────────────────────────
|
|
546
|
+
/**
|
|
547
|
+
* Render a portfolio landing page to a body HTML fragment (no `<html>` shell).
|
|
548
|
+
*
|
|
549
|
+
* Used by the upload path (Phase 2) to store pre-rendered HTML in
|
|
550
|
+
* `users.rendered_portfolio_html` for Phoenix to serve.
|
|
551
|
+
*/
|
|
552
|
+
export function generatePortfolioHtmlFragment(data, templateName) {
|
|
553
|
+
const template = templateName ?? resolveTemplate(undefined, getDefaultTemplate());
|
|
554
|
+
return renderPortfolioHtml(data, template);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Rewrite hardcoded `/{username}/{slug}` absolute project links produced by
|
|
558
|
+
* the current portfolio Liquid templates into relative links that resolve
|
|
559
|
+
* against `projects/{slug}/index.html` when opened from disk.
|
|
560
|
+
*
|
|
561
|
+
* AUDIT FINDING (Phase 1): all 29 portfolio.liquid templates (editorial,
|
|
562
|
+
* blueprint, kinetic, and 26 others) hardcode `/{{ user.username }}/{{ p.slug }}`.
|
|
563
|
+
* Introducing a per-context base URL variable would require changes to
|
|
564
|
+
* `render/liquid.ts` and every template — out of scope for this phase, which
|
|
565
|
+
* only permits editing export.ts, types.ts, and three template files. We
|
|
566
|
+
* rewrite the URLs at the generator boundary instead. Hosted Phoenix serving
|
|
567
|
+
* is unaffected: the render output still contains absolute paths when routed
|
|
568
|
+
* through `renderPortfolioHtml` directly.
|
|
569
|
+
*/
|
|
570
|
+
function rewritePortfolioProjectLinks(html, username) {
|
|
571
|
+
// Escape regex metacharacters in username before embedding.
|
|
572
|
+
const safeUser = username.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
573
|
+
// Matches href="/{username}/{slug}" and href="/{username}/{slug}/{session}"
|
|
574
|
+
const projectOnly = new RegExp(`href="/${safeUser}/([a-z0-9][a-z0-9-]*)"`, 'g');
|
|
575
|
+
const projectSession = new RegExp(`href="/${safeUser}/([a-z0-9][a-z0-9-]*)/([a-z0-9][a-z0-9-]*)"`, 'g');
|
|
576
|
+
return html
|
|
577
|
+
.replace(projectSession, 'href="projects/$1/sessions/$2.html"')
|
|
578
|
+
.replace(projectOnly, 'href="projects/$1/index.html"');
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Generate a complete static portfolio site as a directory tree.
|
|
582
|
+
*
|
|
583
|
+
* Output structure:
|
|
584
|
+
* ```
|
|
585
|
+
* {outputDir}/
|
|
586
|
+
* index.html — portfolio landing page
|
|
587
|
+
* projects/{slug}/index.html — per-project detail page
|
|
588
|
+
* projects/{slug}/sessions/{slug}.html — per-session pages (featured)
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* All internal links are relative; the resulting directory can be opened
|
|
592
|
+
* directly via `file://` without a server. Font loading remains CDN-linked.
|
|
593
|
+
*
|
|
594
|
+
* @param portfolioData Render data for the landing page.
|
|
595
|
+
* @param projects Per-project inputs. Each must have a unique dirName.
|
|
596
|
+
* @param outputDir Absolute output directory. Caller is responsible for
|
|
597
|
+
* validating the path before calling.
|
|
598
|
+
* @param templateName Optional template override (defaults to user setting).
|
|
599
|
+
*/
|
|
600
|
+
export async function generatePortfolioSite(portfolioData, projects, outputDir, templateName) {
|
|
601
|
+
const files = [];
|
|
602
|
+
let totalBytes = 0;
|
|
603
|
+
mkdirSync(outputDir, { recursive: true });
|
|
604
|
+
const template = templateName ?? resolveTemplate(undefined, getDefaultTemplate());
|
|
605
|
+
const username = portfolioData.user.username;
|
|
606
|
+
// ── Landing page ────────────────────────────────────────────
|
|
607
|
+
const portfolioBody = rewritePortfolioProjectLinks(renderPortfolioHtml(portfolioData, template), username);
|
|
608
|
+
const portfolioHtml = buildStandalonePage(portfolioData.user.displayName || username, portfolioBody, {
|
|
609
|
+
description: portfolioData.user.bio?.slice(0, 200) || undefined,
|
|
610
|
+
templateName: template,
|
|
611
|
+
});
|
|
612
|
+
totalBytes += writeAndTrack(join(outputDir, 'index.html'), portfolioHtml, files);
|
|
613
|
+
// ── Per-project sub-sites ───────────────────────────────────
|
|
614
|
+
const projectsRoot = join(outputDir, 'projects');
|
|
615
|
+
mkdirSync(projectsRoot, { recursive: true });
|
|
616
|
+
for (const p of projects) {
|
|
617
|
+
const projectSlug = slugify(p.dirName);
|
|
618
|
+
const projectDir = join(projectsRoot, projectSlug);
|
|
619
|
+
const result = await exportHtml(p.dirName, p.cache, p.sessions, projectDir, username, p.opts);
|
|
620
|
+
files.push(...result.files);
|
|
621
|
+
totalBytes += result.totalBytes;
|
|
622
|
+
}
|
|
623
|
+
return { files, totalBytes, outputPath: outputDir };
|
|
624
|
+
}
|
package/dist/format-utils.js
CHANGED
|
@@ -16,6 +16,11 @@ export function displayNameFromDir(dirName) {
|
|
|
16
16
|
const segments = dirName.split('-').filter(Boolean);
|
|
17
17
|
return segments.length > 0 ? segments[segments.length - 1] : dirName;
|
|
18
18
|
}
|
|
19
|
+
/** Generate a URL-safe slug from a string. */
|
|
20
|
+
export function toSlug(s, maxLen) {
|
|
21
|
+
const slug = s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
22
|
+
return maxLen !== undefined ? slug.slice(0, maxLen) : slug;
|
|
23
|
+
}
|
|
19
24
|
/** Escape LIKE wildcards in user input for safe use in SQL LIKE clauses. */
|
|
20
25
|
export function escapeLikeWildcards(str) {
|
|
21
26
|
return str.replace(/[%_]/g, (c) => `\\${c}`);
|