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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Express router for the GitHub Pages publish target (Phase 5).
|
|
2
|
+
//
|
|
3
|
+
// All routes are authenticated via the existing `getAuthToken` Bearer
|
|
4
|
+
// pattern — the GitHub access token itself lives in the OS keychain via
|
|
5
|
+
// `cli/src/github.ts` and is NEVER returned to the client.
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { getAuthToken } from '../auth.js';
|
|
11
|
+
import { requestDeviceCode, pollForTokenOnce, storeToken, loadToken, deleteToken, listRepos, getAuthenticatedUser, pushSiteToRepo, enablePages, pollPagesBuild, GitHubError, } from '../github.js';
|
|
12
|
+
import { getDefaultTemplate, getPortfolioProfile, hashPortfolioProfile, updatePortfolioPublishTarget, } from '../settings.js';
|
|
13
|
+
import { generatePortfolioSite } from '../export.js';
|
|
14
|
+
import { buildPortfolioRenderData } from './portfolio-render-data.js';
|
|
15
|
+
import { buildProjectDetail } from './context.js';
|
|
16
|
+
import { invalidatePortfolioPreviewCache } from './preview.js';
|
|
17
|
+
const GITHUB_TARGET = 'github';
|
|
18
|
+
function authError(res) {
|
|
19
|
+
res.status(401).json({
|
|
20
|
+
error: { code: 'UNAUTHENTICATED', message: 'Authentication required' },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function handleGitHubError(res, err, fallbackCode) {
|
|
24
|
+
if (err instanceof GitHubError) {
|
|
25
|
+
const status = err.code === 'KEYCHAIN_UNAVAILABLE' ? 503
|
|
26
|
+
: err.code === 'NO_TOKEN' ? 401
|
|
27
|
+
: err.status && err.status >= 400 && err.status < 600 ? err.status
|
|
28
|
+
: 500;
|
|
29
|
+
res.status(status).json({
|
|
30
|
+
error: { code: err.code, message: err.message },
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
res.status(500).json({
|
|
35
|
+
error: { code: fallbackCode, message: err.message },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function createGithubRouter(ctx) {
|
|
39
|
+
const router = Router();
|
|
40
|
+
// ── Device-flow kickoff ────────────────────────────────────────────
|
|
41
|
+
router.post('/api/github/device-code', async (_req, res) => {
|
|
42
|
+
if (!getAuthToken()) {
|
|
43
|
+
authError(res);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
// Scope rationale: public_repo is the minimum needed to push portfolio
|
|
48
|
+
// sites to user-owned public repos and enable Pages on them. We do NOT
|
|
49
|
+
// request the broader 'repo' scope — portfolios are inherently public,
|
|
50
|
+
// and narrowing the scope shrinks the blast radius if a user is ever
|
|
51
|
+
// phished using this app's client_id (RFC 8628 client_ids are public).
|
|
52
|
+
// If users ever request private-repo support, expand to ['repo'] then.
|
|
53
|
+
const body = await requestDeviceCode(['public_repo']);
|
|
54
|
+
res.json({
|
|
55
|
+
device_code: body.device_code,
|
|
56
|
+
user_code: body.user_code,
|
|
57
|
+
verification_uri: body.verification_uri,
|
|
58
|
+
expires_in: body.expires_in,
|
|
59
|
+
interval: body.interval,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
handleGitHubError(res, err, 'DEVICE_CODE_FAILED');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// ── Device-flow single-poll + keychain write ─────────────────────
|
|
67
|
+
// Returns immediately in ALL cases — no blocking loop.
|
|
68
|
+
// The frontend is responsible for calling this on an interval.
|
|
69
|
+
router.post('/api/github/poll-token', async (req, res) => {
|
|
70
|
+
if (!getAuthToken()) {
|
|
71
|
+
authError(res);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { device_code: deviceCode } = (req.body ?? {});
|
|
75
|
+
if (typeof deviceCode !== 'string' || deviceCode.length === 0) {
|
|
76
|
+
res.status(400).json({
|
|
77
|
+
error: { code: 'INVALID_DEVICE_CODE', message: 'device_code is required' },
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const result = await pollForTokenOnce(deviceCode);
|
|
83
|
+
if (result.status === 'success') {
|
|
84
|
+
await storeToken(result.access_token);
|
|
85
|
+
const user = await getAuthenticatedUser(result.access_token);
|
|
86
|
+
res.json({
|
|
87
|
+
status: 'success',
|
|
88
|
+
account: { login: user.login, name: user.name, avatarUrl: user.avatar_url },
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// pending / expired / denied — return status directly, no token in response.
|
|
93
|
+
res.json({ status: result.status });
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
handleGitHubError(res, err, 'TOKEN_POLL_FAILED');
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// ── Connected account (GET + DELETE) ──────────────────────────────
|
|
100
|
+
router.get('/api/github/account', async (_req, res) => {
|
|
101
|
+
if (!getAuthToken()) {
|
|
102
|
+
authError(res);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const token = await loadToken();
|
|
107
|
+
if (!token) {
|
|
108
|
+
res.json({ account: null });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const user = await getAuthenticatedUser(token);
|
|
112
|
+
res.json({
|
|
113
|
+
account: { login: user.login, name: user.name, avatarUrl: user.avatar_url },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
handleGitHubError(res, err, 'GITHUB_API_FAILED');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
router.delete('/api/github/account', async (_req, res) => {
|
|
121
|
+
if (!getAuthToken()) {
|
|
122
|
+
authError(res);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await deleteToken();
|
|
127
|
+
res.json({ ok: true });
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
handleGitHubError(res, err, 'KEYCHAIN_UNAVAILABLE');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// ── Repo list ─────────────────────────────────────────────────────
|
|
134
|
+
router.get('/api/github/repos', async (_req, res) => {
|
|
135
|
+
if (!getAuthToken()) {
|
|
136
|
+
authError(res);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const token = await loadToken();
|
|
141
|
+
if (!token) {
|
|
142
|
+
res.status(401).json({
|
|
143
|
+
error: { code: 'NO_GITHUB_TOKEN', message: 'GitHub account not connected' },
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const repos = await listRepos(token);
|
|
148
|
+
res.json({ repos });
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
handleGitHubError(res, err, 'GITHUB_API_FAILED');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// ── Publish: render portfolio -> push to repo -> enable Pages ─────
|
|
155
|
+
router.post('/api/github/publish', async (req, res) => {
|
|
156
|
+
const auth = getAuthToken();
|
|
157
|
+
if (!auth) {
|
|
158
|
+
authError(res);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { owner, repo, branch } = (req.body ?? {});
|
|
162
|
+
if (typeof owner !== 'string' || owner.length === 0
|
|
163
|
+
|| typeof repo !== 'string' || repo.length === 0) {
|
|
164
|
+
res.status(400).json({
|
|
165
|
+
error: { code: 'INVALID_TARGET', message: 'owner and repo are required strings' },
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const branchName = typeof branch === 'string' && branch.length > 0 ? branch : 'gh-pages';
|
|
170
|
+
let tempDir = null;
|
|
171
|
+
try {
|
|
172
|
+
const token = await loadToken();
|
|
173
|
+
if (!token) {
|
|
174
|
+
res.status(401).json({
|
|
175
|
+
error: { code: 'NO_GITHUB_TOKEN', message: 'GitHub account not connected' },
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Build portfolio site into a temp directory.
|
|
180
|
+
const templateName = getDefaultTemplate() || 'editorial';
|
|
181
|
+
const { renderData } = await buildPortfolioRenderData(ctx, auth);
|
|
182
|
+
const rawProjects = await ctx.getProjects();
|
|
183
|
+
const projectInputs = [];
|
|
184
|
+
for (const rawProj of rawProjects) {
|
|
185
|
+
try {
|
|
186
|
+
const detail = buildProjectDetail(ctx.db, rawProj);
|
|
187
|
+
const cache = detail.enhanceCache ?? {
|
|
188
|
+
fingerprint: 'gh-publish',
|
|
189
|
+
enhancedAt: new Date().toISOString(),
|
|
190
|
+
selectedSessionIds: detail.sessions.map((s) => s.id),
|
|
191
|
+
result: { narrative: '', arc: [], skills: [], timeline: [], questions: [] },
|
|
192
|
+
};
|
|
193
|
+
const proj = detail.project;
|
|
194
|
+
projectInputs.push({
|
|
195
|
+
dirName: rawProj.dirName,
|
|
196
|
+
cache,
|
|
197
|
+
sessions: detail.sessions,
|
|
198
|
+
opts: {
|
|
199
|
+
totalFilesChanged: proj.totalFiles,
|
|
200
|
+
totalAgentDurationMinutes: proj.totalAgentDuration,
|
|
201
|
+
totalInputTokens: proj.totalInputTokens,
|
|
202
|
+
totalOutputTokens: proj.totalOutputTokens,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (projErr) {
|
|
207
|
+
console.warn(`[github-publish] skipping project ${rawProj.dirName}:`, projErr.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
tempDir = mkdtempSync(join(tmpdir(), 'heyiam-gh-'));
|
|
211
|
+
await generatePortfolioSite(renderData, projectInputs, tempDir, templateName);
|
|
212
|
+
await pushSiteToRepo({
|
|
213
|
+
token, owner, repo, branch: branchName, sourceDir: tempDir,
|
|
214
|
+
});
|
|
215
|
+
await enablePages({ token, owner, repo, branch: branchName });
|
|
216
|
+
await pollPagesBuild({ token, owner, repo });
|
|
217
|
+
const url = `https://${owner}.github.io/${repo}/`;
|
|
218
|
+
const publishedAt = new Date().toISOString();
|
|
219
|
+
const profile = getPortfolioProfile();
|
|
220
|
+
const hash = hashPortfolioProfile(profile);
|
|
221
|
+
updatePortfolioPublishTarget(GITHUB_TARGET, {
|
|
222
|
+
lastPublishedAt: publishedAt,
|
|
223
|
+
lastPublishedProfileHash: hash,
|
|
224
|
+
lastPublishedProfile: profile,
|
|
225
|
+
config: { owner, repo, branch: branchName },
|
|
226
|
+
url,
|
|
227
|
+
lastError: undefined,
|
|
228
|
+
lastErrorAt: undefined,
|
|
229
|
+
});
|
|
230
|
+
invalidatePortfolioPreviewCache();
|
|
231
|
+
res.json({ ok: true, url, publishedAt, hash });
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
235
|
+
try {
|
|
236
|
+
updatePortfolioPublishTarget(GITHUB_TARGET, {
|
|
237
|
+
lastError: message,
|
|
238
|
+
lastErrorAt: new Date().toISOString(),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch { /* do not mask original error */ }
|
|
242
|
+
handleGitHubError(res, err, 'GITHUB_PUBLISH_FAILED');
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
if (tempDir) {
|
|
246
|
+
try {
|
|
247
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
catch { /* best effort */ }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return router;
|
|
254
|
+
}
|
package/dist/routes/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { createRouteContext } from './context.js';
|
|
|
2
2
|
export { createProjectsRouter } from './projects.js';
|
|
3
3
|
export { createEnhanceRouter } from './enhance.js';
|
|
4
4
|
export { createPublishRouter } from './publish.js';
|
|
5
|
+
export { createDeleteRouter } from './delete.js';
|
|
5
6
|
export { createSearchRouter } from './search.js';
|
|
6
7
|
export { createSessionsRouter } from './sessions.js';
|
|
7
8
|
export { createArchiveRouter } from './archive.js';
|
|
@@ -10,3 +11,4 @@ export { createSettingsRouter } from './settings.js';
|
|
|
10
11
|
export { createExportRouter } from './export.js';
|
|
11
12
|
export { createPreviewRouter } from './preview.js';
|
|
12
13
|
export { createDashboardRouter } from './dashboard.js';
|
|
14
|
+
export { createGithubRouter } from './github.js';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getPortfolioProfile, loadProjectEnhanceResult } from '../settings.js';
|
|
2
|
+
import { getSessionsByProject, getAllProjectStats } from '../db.js';
|
|
3
|
+
import { displayNameFromDir } from '../sync.js';
|
|
4
|
+
import { toSlug } from '../format-utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Assemble the `PortfolioRenderData` payload from local project data.
|
|
7
|
+
*
|
|
8
|
+
* Shared between:
|
|
9
|
+
* - `POST /api/portfolio/upload` (Phase 2, hosted heyi.am publish)
|
|
10
|
+
* - `POST /api/portfolio/export` (Phase 4, static folder export)
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the preview route's assembly logic. Projects that fail to load are
|
|
13
|
+
* silently skipped — the portfolio still publishes with whatever succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export async function buildPortfolioRenderData(ctx, auth) {
|
|
16
|
+
const profile = getPortfolioProfile();
|
|
17
|
+
const allRawProjects = await ctx.getProjects();
|
|
18
|
+
// Build a recency map from DB stats so the default-when-empty branch of
|
|
19
|
+
// applyPortfolioProjectFilter can rank projects by "user's most recent
|
|
20
|
+
// work" (latest non-subagent session start_time).
|
|
21
|
+
const recencyByDir = new Map();
|
|
22
|
+
try {
|
|
23
|
+
for (const s of getAllProjectStats(ctx.db)) {
|
|
24
|
+
recencyByDir.set(s.projectDir, s.latestDate || '');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { /* DB may be empty on first run; default branch will fall back */ }
|
|
28
|
+
const rawProjects = applyPortfolioProjectFilter(allRawProjects, profile.projectsOnPortfolio, { getRecency: (p) => recencyByDir.get(p.dirName) });
|
|
29
|
+
const portfolioProjects = [];
|
|
30
|
+
const projectCaches = new Map();
|
|
31
|
+
let totalDuration = 0;
|
|
32
|
+
let totalAgentDuration = 0;
|
|
33
|
+
let totalLoc = 0;
|
|
34
|
+
let totalSessions = 0;
|
|
35
|
+
for (const rawProj of rawProjects) {
|
|
36
|
+
try {
|
|
37
|
+
const proj = await ctx.getProjectWithStats(rawProj);
|
|
38
|
+
const cached = loadProjectEnhanceResult(rawProj.dirName);
|
|
39
|
+
const projDuration = proj.totalDuration || 0;
|
|
40
|
+
const projAgentDuration = proj.totalAgentDuration || 0;
|
|
41
|
+
const projLoc = proj.totalLoc || 0;
|
|
42
|
+
const projSessions = proj.sessionCount || 0;
|
|
43
|
+
totalDuration += projDuration;
|
|
44
|
+
totalAgentDuration += projAgentDuration;
|
|
45
|
+
totalLoc += projLoc;
|
|
46
|
+
totalSessions += projSessions;
|
|
47
|
+
const title = cached?.title
|
|
48
|
+
|| proj.name || displayNameFromDir(rawProj.dirName);
|
|
49
|
+
const dbSessions = getSessionsByProject(ctx.db, rawProj.dirName);
|
|
50
|
+
const sessionActivity = dbSessions
|
|
51
|
+
.filter((s) => !s.is_subagent)
|
|
52
|
+
.map((s) => ({
|
|
53
|
+
date: s.start_time || '',
|
|
54
|
+
loc: (s.loc_added || 0) + (s.loc_removed || 0),
|
|
55
|
+
durationMinutes: s.duration_minutes || 0,
|
|
56
|
+
}));
|
|
57
|
+
portfolioProjects.push({
|
|
58
|
+
slug: toSlug(title),
|
|
59
|
+
title,
|
|
60
|
+
narrative: cached?.result?.narrative || proj.description || '',
|
|
61
|
+
totalSessions: projSessions,
|
|
62
|
+
totalLoc: projLoc,
|
|
63
|
+
totalDurationMinutes: projDuration,
|
|
64
|
+
totalAgentDurationMinutes: projAgentDuration,
|
|
65
|
+
totalFilesChanged: proj.totalFiles || 0,
|
|
66
|
+
skills: cached?.result?.skills || proj.skills || [],
|
|
67
|
+
publishedCount: 0,
|
|
68
|
+
sessions: sessionActivity,
|
|
69
|
+
});
|
|
70
|
+
projectCaches.set(rawProj.dirName, { dirName: rawProj.dirName, cache: cached });
|
|
71
|
+
}
|
|
72
|
+
catch { /* skip projects that fail */ }
|
|
73
|
+
}
|
|
74
|
+
const renderData = {
|
|
75
|
+
user: {
|
|
76
|
+
username: auth.username,
|
|
77
|
+
accent: profile.accent || '#084471',
|
|
78
|
+
displayName: profile.displayName || '',
|
|
79
|
+
bio: profile.bio || '',
|
|
80
|
+
location: profile.location || '',
|
|
81
|
+
status: 'active',
|
|
82
|
+
email: profile.email,
|
|
83
|
+
phone: profile.phone,
|
|
84
|
+
photoUrl: profile.photoBase64 || undefined,
|
|
85
|
+
linkedinUrl: profile.linkedinUrl,
|
|
86
|
+
githubUrl: profile.githubUrl,
|
|
87
|
+
twitterHandle: profile.twitterHandle,
|
|
88
|
+
websiteUrl: profile.websiteUrl,
|
|
89
|
+
resumeUrl: profile.resumeBase64 ? '#' : undefined,
|
|
90
|
+
},
|
|
91
|
+
projects: portfolioProjects,
|
|
92
|
+
totalDurationMinutes: totalDuration,
|
|
93
|
+
totalAgentDurationMinutes: totalAgentDuration || undefined,
|
|
94
|
+
totalLoc,
|
|
95
|
+
totalSessions,
|
|
96
|
+
};
|
|
97
|
+
return { renderData, projectCaches, filteredProjects: rawProjects };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Apply the user-curated `projectsOnPortfolio` list to a raw project list.
|
|
101
|
+
*
|
|
102
|
+
* Pure function — no I/O. Lives here so it can be unit-tested without
|
|
103
|
+
* standing up a RouteContext.
|
|
104
|
+
*
|
|
105
|
+
* Semantics:
|
|
106
|
+
* - Empty/missing list: default to the `defaultLimit` (3) most recently
|
|
107
|
+
* active projects, ranked by `getRecency` (descending). If fewer than
|
|
108
|
+
* `defaultLimit` projects exist, return all of them. If `getRecency` is
|
|
109
|
+
* not provided, falls back to reverse-alphabetic order on `dirName`
|
|
110
|
+
* (an unfortunate fallback — callers should provide a real recency
|
|
111
|
+
* accessor).
|
|
112
|
+
* - Non-empty list:
|
|
113
|
+
* - Filter out projects whose entry has `included === false`.
|
|
114
|
+
* - Sort the remaining matched projects by `order` ascending.
|
|
115
|
+
* - Projects present in the source list but missing from the curated
|
|
116
|
+
* list (e.g. newly imported since the user last edited) are appended
|
|
117
|
+
* at the end in source order, treated as `included: true`.
|
|
118
|
+
*
|
|
119
|
+
* NOTE: The default-when-empty branch is duplicated in
|
|
120
|
+
* `cli/app/src/components/PortfolioWorkspace.tsx` (HydratePortfolioStore)
|
|
121
|
+
* because the frontend bundler does not reach into `cli/src/`. Keep the
|
|
122
|
+
* two implementations in sync.
|
|
123
|
+
*/
|
|
124
|
+
export const PORTFOLIO_DEFAULT_PROJECT_LIMIT = 3;
|
|
125
|
+
export function applyPortfolioProjectFilter(projects, curated, options = {}) {
|
|
126
|
+
if (!curated || curated.length === 0) {
|
|
127
|
+
const limit = options.defaultLimit ?? PORTFOLIO_DEFAULT_PROJECT_LIMIT;
|
|
128
|
+
if (projects.length <= limit)
|
|
129
|
+
return projects;
|
|
130
|
+
const getRecency = options.getRecency;
|
|
131
|
+
const ranked = projects.slice().sort((a, b) => {
|
|
132
|
+
if (getRecency) {
|
|
133
|
+
const ra = getRecency(a) || '';
|
|
134
|
+
const rb = getRecency(b) || '';
|
|
135
|
+
if (ra !== rb)
|
|
136
|
+
return rb.localeCompare(ra); // descending
|
|
137
|
+
}
|
|
138
|
+
// Fallback / tiebreaker: reverse-alphabetic on dirName.
|
|
139
|
+
return b.dirName.localeCompare(a.dirName);
|
|
140
|
+
});
|
|
141
|
+
return ranked.slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
const byId = new Map();
|
|
144
|
+
for (const entry of curated)
|
|
145
|
+
byId.set(entry.projectId, entry);
|
|
146
|
+
const matched = [];
|
|
147
|
+
const unmatched = [];
|
|
148
|
+
for (const proj of projects) {
|
|
149
|
+
const entry = byId.get(proj.dirName);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
unmatched.push(proj);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (entry.included === false)
|
|
155
|
+
continue;
|
|
156
|
+
matched.push({ proj, order: entry.order });
|
|
157
|
+
}
|
|
158
|
+
matched.sort((a, b) => a.order - b.order);
|
|
159
|
+
return [...matched.map((m) => m.proj), ...unmatched];
|
|
160
|
+
}
|