@wilsong/shiplog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # ShipLog
2
+
3
+ **What did your AI build while you were at the gym?**
4
+
5
+ Session reports for vibe-coding developers. One beautiful branded email summarizing everything your AI coding assistant shipped — commits, features, fixes, deployments, and what's still pending.
6
+
7
+ ## The Problem
8
+
9
+ You close your laptop. You go to the gym. You come back and:
10
+ - 47 individual GitHub commit notifications (noise)
11
+ - No idea what state your codebase is in
12
+ - Can't tell your team/investors what shipped today
13
+ - The AI coded for 3 hours and you have no summary
14
+
15
+ ## The Solution
16
+
17
+ ```bash
18
+ npx shiplog --email you@example.com
19
+ ```
20
+
21
+ ShipLog watches your git activity, analyzes what changed, and sends you ONE branded HTML email with:
22
+ - Total commits and files changed
23
+ - Features shipped vs bugs fixed vs refactors
24
+ - Database migrations applied
25
+ - Edge functions deployed
26
+ - Build status (pass/fail)
27
+ - What's still pending
28
+ - Shipping velocity over time
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ npm install -g @wilsong/shiplog
34
+ ```
35
+
36
+ ### Option 1: CLI (run after a session)
37
+ ```bash
38
+ shiplog send --email you@example.com --since "4 hours ago"
39
+ ```
40
+
41
+ ### Option 2: Watch mode (auto-send when idle)
42
+ ```bash
43
+ shiplog watch --email you@example.com --idle-timeout 30m
44
+ ```
45
+
46
+ ### Option 3: Programmatic (in your AI workflow)
47
+ ```typescript
48
+ import { ShipLog } from '@wilsong/shiplog';
49
+
50
+ const log = new ShipLog({
51
+ email: 'you@example.com',
52
+ resendApiKey: process.env.RESEND_API_KEY,
53
+ brandName: 'Your Company',
54
+ brandColor: '#0077B5',
55
+ });
56
+
57
+ // Analyze recent git activity and send report
58
+ await log.analyzeAndSend({ since: '4 hours ago' });
59
+ ```
60
+
61
+ ## What Gets Analyzed
62
+
63
+ | Signal | Source |
64
+ |--------|--------|
65
+ | Commits | `git log` with diff stats |
66
+ | Features vs Fixes | Commit message convention detection (feat/fix/refactor) |
67
+ | Files Changed | Grouped by directory (components, API routes, DB, etc.) |
68
+ | Build Status | Detects `next build` / `npm run build` exit codes |
69
+ | Migrations | Supabase migration files detected |
70
+ | Edge Functions | Supabase edge function deploys detected |
71
+ | Test Results | Playwright / Vitest / Jest results if available |
72
+ | Dependencies | `package.json` changes (new deps added) |
73
+
74
+ ## Email Template
75
+
76
+ The email is a branded HTML report that looks professional enough to forward to:
77
+ - Your team (daily standup replacement)
78
+ - Your investors (weekly shipping updates)
79
+ - Your clients (progress reports)
80
+ - Yourself (session memory for ADHD developers)
81
+
82
+ ## Configuration
83
+
84
+ Create a `.shiplog.json` in your project root:
85
+
86
+ ```json
87
+ {
88
+ "email": "you@example.com",
89
+ "cc": ["team@example.com", "investor@example.com"],
90
+ "resendApiKey": "re_...",
91
+ "brandName": "Drivia",
92
+ "brandColor": "#0077B5",
93
+ "idleTimeout": "30m",
94
+ "includeLinkedInDraft": true,
95
+ "includeSlackWebhook": "https://hooks.slack.com/...",
96
+ "commitConventions": true,
97
+ "groupByDirectory": true
98
+ }
99
+ ```
100
+
101
+ ## Integrations (Roadmap)
102
+
103
+ - [x] Email via Resend
104
+ - [ ] Slack webhook
105
+ - [ ] Discord webhook
106
+ - [ ] Linear ticket updates
107
+ - [ ] Notion page creation
108
+ - [ ] LinkedIn post draft generation
109
+ - [ ] GitHub Release auto-creation
110
+ - [ ] Investor update template (monthly)
111
+
112
+ ## Why This Exists
113
+
114
+ Built by [Wilson Guenther](https://drivia.consulting) — a founder with ADHD who codes with AI for 6+ hours a day and needed a way to remember what shipped.
115
+
116
+ The vibe-coding revolution means developers ship more in a day than ever before. But if you can't communicate what you shipped, it doesn't count. ShipLog bridges the gap between building and communicating.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,200 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/index.ts
9
+ import simpleGit from "simple-git";
10
+ import { Resend } from "resend";
11
+ function categorizeCommit(message) {
12
+ const lower = message.toLowerCase();
13
+ if (lower.startsWith("feat") || lower.includes("add") || lower.includes("implement") || lower.includes("ship")) return "feature";
14
+ if (lower.startsWith("fix") || lower.includes("bug") || lower.includes("patch") || lower.includes("hotfix")) return "fix";
15
+ if (lower.startsWith("refactor") || lower.includes("cleanup") || lower.includes("reorganize") || lower.includes("rename")) return "refactor";
16
+ return "other";
17
+ }
18
+ function humanDuration(from, to) {
19
+ const diffMs = to.getTime() - from.getTime();
20
+ const hours = Math.floor(diffMs / 36e5);
21
+ const minutes = Math.floor(diffMs % 36e5 / 6e4);
22
+ if (hours > 0) return `${hours}h ${minutes}m`;
23
+ return `${minutes}m`;
24
+ }
25
+ var ShipLog = class {
26
+ git;
27
+ resend;
28
+ config;
29
+ constructor(config) {
30
+ this.config = {
31
+ brandName: "ShipLog",
32
+ brandColor: "#0077B5",
33
+ fromEmail: "ShipLog <updates@shiplog.dev>",
34
+ ...config
35
+ };
36
+ this.git = simpleGit(config.repoPath || process.cwd());
37
+ this.resend = new Resend(config.resendApiKey);
38
+ }
39
+ /**
40
+ * Analyze git history since a given time and return a structured report
41
+ */
42
+ async analyze(options = {}) {
43
+ const since = options.since || "8 hours ago";
44
+ const branch = options.branch || "HEAD";
45
+ const log = await this.git.log({
46
+ from: "",
47
+ to: branch,
48
+ "--since": since,
49
+ "--stat": null
50
+ });
51
+ const features = [];
52
+ const fixes = [];
53
+ const refactors = [];
54
+ const other = [];
55
+ const authors = {};
56
+ const directories = {};
57
+ let totalInsertions = 0;
58
+ let totalDeletions = 0;
59
+ let totalFilesChanged = 0;
60
+ for (const commit of log.all) {
61
+ const msg = commit.message.split("\n")[0];
62
+ const category = categorizeCommit(msg);
63
+ switch (category) {
64
+ case "feature":
65
+ features.push(msg);
66
+ break;
67
+ case "fix":
68
+ fixes.push(msg);
69
+ break;
70
+ case "refactor":
71
+ refactors.push(msg);
72
+ break;
73
+ default:
74
+ other.push(msg);
75
+ break;
76
+ }
77
+ const author = commit.author_name || "Unknown";
78
+ authors[author] = (authors[author] || 0) + 1;
79
+ const diffStat = commit.diff?.changed || 0;
80
+ const ins = commit.diff?.insertions || 0;
81
+ const del = commit.diff?.deletions || 0;
82
+ totalFilesChanged += diffStat;
83
+ totalInsertions += ins;
84
+ totalDeletions += del;
85
+ if (commit.diff?.files) {
86
+ for (const file of commit.diff.files) {
87
+ const dir = file.file.split("/").slice(0, 2).join("/");
88
+ directories[dir] = (directories[dir] || 0) + 1;
89
+ }
90
+ }
91
+ }
92
+ const commits = log.all;
93
+ const from = commits.length > 0 ? commits[commits.length - 1].date : (/* @__PURE__ */ new Date()).toISOString();
94
+ const to = commits.length > 0 ? commits[0].date : (/* @__PURE__ */ new Date()).toISOString();
95
+ const duration = humanDuration(new Date(from), new Date(to));
96
+ const report = {
97
+ totalCommits: log.total,
98
+ filesChanged: totalFilesChanged,
99
+ insertions: totalInsertions,
100
+ deletions: totalDeletions,
101
+ features,
102
+ fixes,
103
+ refactors,
104
+ other,
105
+ authors,
106
+ directories,
107
+ timeRange: { from, to },
108
+ duration
109
+ };
110
+ if (this.config.includeLinkedInDraft) {
111
+ report.linkedInDraft = this.generateLinkedInDraft(report);
112
+ }
113
+ return report;
114
+ }
115
+ /**
116
+ * Generate a branded HTML email from a session report
117
+ */
118
+ buildEmail(report) {
119
+ const { brandName, brandColor } = this.config;
120
+ const featureList = report.features.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
121
+ const fixList = report.fixes.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
122
+ const refactorList = report.refactors.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
123
+ const otherList = report.other.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
124
+ const sections = [];
125
+ if (report.features.length > 0) sections.push(`<h2 style="color:#22c55e;border-bottom:2px solid #22c55e;padding-bottom:4px">\u2728 Features (${report.features.length})</h2><ul style="font-size:13px">${featureList}</ul>`);
126
+ if (report.fixes.length > 0) sections.push(`<h2 style="color:#ef4444;border-bottom:2px solid #ef4444;padding-bottom:4px">\u{1F527} Fixes (${report.fixes.length})</h2><ul style="font-size:13px">${fixList}</ul>`);
127
+ if (report.refactors.length > 0) sections.push(`<h2 style="color:#8b5cf6;border-bottom:2px solid #8b5cf6;padding-bottom:4px">\u267B\uFE0F Refactors (${report.refactors.length})</h2><ul style="font-size:13px">${refactorList}</ul>`);
128
+ if (report.other.length > 0) sections.push(`<h2 style="color:#6b7280;border-bottom:2px solid #6b7280;padding-bottom:4px">\u{1F4DD} Other (${report.other.length})</h2><ul style="font-size:13px">${otherList}</ul>`);
129
+ const authorRows = Object.entries(report.authors).map(([name, count]) => `<span style="margin-right:12px">${escapeHtml(name)}: <strong>${count}</strong></span>`).join("");
130
+ return `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1a1a2e">
131
+ <div style="background:${brandColor};color:white;padding:16px 20px;border-radius:12px 12px 0 0">
132
+ <h1 style="margin:0;font-size:20px">${escapeHtml(brandName)} \u2014 Session Report</h1>
133
+ <p style="margin:4px 0 0;opacity:0.85;font-size:13px">${report.timeRange.from.split("T")[0]} \u2022 ${report.duration} session</p>
134
+ </div>
135
+
136
+ <div style="background:#f8fafc;padding:16px 20px;border:1px solid #e2e8f0;border-top:none">
137
+ <div style="display:flex;gap:20px;text-align:center">
138
+ <div><div style="font-size:28px;font-weight:bold;color:${brandColor}">${report.totalCommits}</div><div style="font-size:11px;color:#64748b">Commits</div></div>
139
+ <div><div style="font-size:28px;font-weight:bold;color:#22c55e">${report.features.length}</div><div style="font-size:11px;color:#64748b">Features</div></div>
140
+ <div><div style="font-size:28px;font-weight:bold;color:#ef4444">${report.fixes.length}</div><div style="font-size:11px;color:#64748b">Fixes</div></div>
141
+ <div><div style="font-size:28px;font-weight:bold;color:#64748b">+${report.insertions}/-${report.deletions}</div><div style="font-size:11px;color:#64748b">Lines</div></div>
142
+ </div>
143
+ </div>
144
+
145
+ <div style="padding:16px 20px;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
146
+ ${sections.join("\n")}
147
+ <div style="margin-top:16px;padding:12px;background:#f1f5f9;border-radius:8px;font-size:12px;color:#475569">
148
+ <strong>Authors:</strong> ${authorRows}
149
+ </div>
150
+ </div>
151
+
152
+ <p style="color:#94a3b8;font-size:10px;text-align:center;margin-top:16px">Powered by <a href="https://shiplog.dev" style="color:${brandColor}">ShipLog</a> \u2014 session reports for vibe-coding developers</p>
153
+ </div>`;
154
+ }
155
+ /**
156
+ * Analyze git history and send the email report
157
+ */
158
+ async analyzeAndSend(options = {}) {
159
+ const report = await this.analyze(options);
160
+ if (report.totalCommits === 0) {
161
+ throw new Error("No commits found in the specified time range");
162
+ }
163
+ const html = this.buildEmail(report);
164
+ const subject = `${this.config.brandName}: ${report.totalCommits} commits \u2014 ${report.features.length} features, ${report.fixes.length} fixes`;
165
+ const { data, error } = await this.resend.emails.send({
166
+ from: this.config.fromEmail,
167
+ to: this.config.email,
168
+ cc: this.config.cc,
169
+ subject,
170
+ html
171
+ });
172
+ if (error) throw new Error(`Email send failed: ${error.message}`);
173
+ return { report, emailId: data?.id || "sent" };
174
+ }
175
+ /**
176
+ * Generate a LinkedIn post draft from the session report
177
+ */
178
+ generateLinkedInDraft(report) {
179
+ const topFeatures = report.features.slice(0, 3).map((f) => `\u2192 ${f}`).join("\n");
180
+ return `Shipped ${report.totalCommits} commits today.
181
+
182
+ ${report.features.length} features. ${report.fixes.length} fixes. ${report.duration} of building.
183
+
184
+ ${topFeatures}
185
+
186
+ The best developers don't just code \u2014 they ship. And they communicate what they shipped.
187
+
188
+ What did you build today?
189
+
190
+ #BuildInPublic #ShipLog #FounderLife`;
191
+ }
192
+ };
193
+ function escapeHtml(str) {
194
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
195
+ }
196
+
197
+ export {
198
+ __require,
199
+ ShipLog
200
+ };
package/dist/cli.cjs ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+ var import_chalk = __toESM(require("chalk"), 1);
29
+ var import_ora = __toESM(require("ora"), 1);
30
+
31
+ // src/index.ts
32
+ var import_simple_git = __toESM(require("simple-git"), 1);
33
+ var import_resend = require("resend");
34
+ function categorizeCommit(message) {
35
+ const lower = message.toLowerCase();
36
+ if (lower.startsWith("feat") || lower.includes("add") || lower.includes("implement") || lower.includes("ship")) return "feature";
37
+ if (lower.startsWith("fix") || lower.includes("bug") || lower.includes("patch") || lower.includes("hotfix")) return "fix";
38
+ if (lower.startsWith("refactor") || lower.includes("cleanup") || lower.includes("reorganize") || lower.includes("rename")) return "refactor";
39
+ return "other";
40
+ }
41
+ function humanDuration(from, to) {
42
+ const diffMs = to.getTime() - from.getTime();
43
+ const hours = Math.floor(diffMs / 36e5);
44
+ const minutes = Math.floor(diffMs % 36e5 / 6e4);
45
+ if (hours > 0) return `${hours}h ${minutes}m`;
46
+ return `${minutes}m`;
47
+ }
48
+ var ShipLog = class {
49
+ git;
50
+ resend;
51
+ config;
52
+ constructor(config) {
53
+ this.config = {
54
+ brandName: "ShipLog",
55
+ brandColor: "#0077B5",
56
+ fromEmail: "ShipLog <updates@shiplog.dev>",
57
+ ...config
58
+ };
59
+ this.git = (0, import_simple_git.default)(config.repoPath || process.cwd());
60
+ this.resend = new import_resend.Resend(config.resendApiKey);
61
+ }
62
+ /**
63
+ * Analyze git history since a given time and return a structured report
64
+ */
65
+ async analyze(options = {}) {
66
+ const since = options.since || "8 hours ago";
67
+ const branch = options.branch || "HEAD";
68
+ const log = await this.git.log({
69
+ from: "",
70
+ to: branch,
71
+ "--since": since,
72
+ "--stat": null
73
+ });
74
+ const features = [];
75
+ const fixes = [];
76
+ const refactors = [];
77
+ const other = [];
78
+ const authors = {};
79
+ const directories = {};
80
+ let totalInsertions = 0;
81
+ let totalDeletions = 0;
82
+ let totalFilesChanged = 0;
83
+ for (const commit of log.all) {
84
+ const msg = commit.message.split("\n")[0];
85
+ const category = categorizeCommit(msg);
86
+ switch (category) {
87
+ case "feature":
88
+ features.push(msg);
89
+ break;
90
+ case "fix":
91
+ fixes.push(msg);
92
+ break;
93
+ case "refactor":
94
+ refactors.push(msg);
95
+ break;
96
+ default:
97
+ other.push(msg);
98
+ break;
99
+ }
100
+ const author = commit.author_name || "Unknown";
101
+ authors[author] = (authors[author] || 0) + 1;
102
+ const diffStat = commit.diff?.changed || 0;
103
+ const ins = commit.diff?.insertions || 0;
104
+ const del = commit.diff?.deletions || 0;
105
+ totalFilesChanged += diffStat;
106
+ totalInsertions += ins;
107
+ totalDeletions += del;
108
+ if (commit.diff?.files) {
109
+ for (const file of commit.diff.files) {
110
+ const dir = file.file.split("/").slice(0, 2).join("/");
111
+ directories[dir] = (directories[dir] || 0) + 1;
112
+ }
113
+ }
114
+ }
115
+ const commits = log.all;
116
+ const from = commits.length > 0 ? commits[commits.length - 1].date : (/* @__PURE__ */ new Date()).toISOString();
117
+ const to = commits.length > 0 ? commits[0].date : (/* @__PURE__ */ new Date()).toISOString();
118
+ const duration = humanDuration(new Date(from), new Date(to));
119
+ const report = {
120
+ totalCommits: log.total,
121
+ filesChanged: totalFilesChanged,
122
+ insertions: totalInsertions,
123
+ deletions: totalDeletions,
124
+ features,
125
+ fixes,
126
+ refactors,
127
+ other,
128
+ authors,
129
+ directories,
130
+ timeRange: { from, to },
131
+ duration
132
+ };
133
+ if (this.config.includeLinkedInDraft) {
134
+ report.linkedInDraft = this.generateLinkedInDraft(report);
135
+ }
136
+ return report;
137
+ }
138
+ /**
139
+ * Generate a branded HTML email from a session report
140
+ */
141
+ buildEmail(report) {
142
+ const { brandName, brandColor } = this.config;
143
+ const featureList = report.features.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
144
+ const fixList = report.fixes.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
145
+ const refactorList = report.refactors.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
146
+ const otherList = report.other.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
147
+ const sections = [];
148
+ if (report.features.length > 0) sections.push(`<h2 style="color:#22c55e;border-bottom:2px solid #22c55e;padding-bottom:4px">\u2728 Features (${report.features.length})</h2><ul style="font-size:13px">${featureList}</ul>`);
149
+ if (report.fixes.length > 0) sections.push(`<h2 style="color:#ef4444;border-bottom:2px solid #ef4444;padding-bottom:4px">\u{1F527} Fixes (${report.fixes.length})</h2><ul style="font-size:13px">${fixList}</ul>`);
150
+ if (report.refactors.length > 0) sections.push(`<h2 style="color:#8b5cf6;border-bottom:2px solid #8b5cf6;padding-bottom:4px">\u267B\uFE0F Refactors (${report.refactors.length})</h2><ul style="font-size:13px">${refactorList}</ul>`);
151
+ if (report.other.length > 0) sections.push(`<h2 style="color:#6b7280;border-bottom:2px solid #6b7280;padding-bottom:4px">\u{1F4DD} Other (${report.other.length})</h2><ul style="font-size:13px">${otherList}</ul>`);
152
+ const authorRows = Object.entries(report.authors).map(([name, count]) => `<span style="margin-right:12px">${escapeHtml(name)}: <strong>${count}</strong></span>`).join("");
153
+ return `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1a1a2e">
154
+ <div style="background:${brandColor};color:white;padding:16px 20px;border-radius:12px 12px 0 0">
155
+ <h1 style="margin:0;font-size:20px">${escapeHtml(brandName)} \u2014 Session Report</h1>
156
+ <p style="margin:4px 0 0;opacity:0.85;font-size:13px">${report.timeRange.from.split("T")[0]} \u2022 ${report.duration} session</p>
157
+ </div>
158
+
159
+ <div style="background:#f8fafc;padding:16px 20px;border:1px solid #e2e8f0;border-top:none">
160
+ <div style="display:flex;gap:20px;text-align:center">
161
+ <div><div style="font-size:28px;font-weight:bold;color:${brandColor}">${report.totalCommits}</div><div style="font-size:11px;color:#64748b">Commits</div></div>
162
+ <div><div style="font-size:28px;font-weight:bold;color:#22c55e">${report.features.length}</div><div style="font-size:11px;color:#64748b">Features</div></div>
163
+ <div><div style="font-size:28px;font-weight:bold;color:#ef4444">${report.fixes.length}</div><div style="font-size:11px;color:#64748b">Fixes</div></div>
164
+ <div><div style="font-size:28px;font-weight:bold;color:#64748b">+${report.insertions}/-${report.deletions}</div><div style="font-size:11px;color:#64748b">Lines</div></div>
165
+ </div>
166
+ </div>
167
+
168
+ <div style="padding:16px 20px;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px">
169
+ ${sections.join("\n")}
170
+ <div style="margin-top:16px;padding:12px;background:#f1f5f9;border-radius:8px;font-size:12px;color:#475569">
171
+ <strong>Authors:</strong> ${authorRows}
172
+ </div>
173
+ </div>
174
+
175
+ <p style="color:#94a3b8;font-size:10px;text-align:center;margin-top:16px">Powered by <a href="https://shiplog.dev" style="color:${brandColor}">ShipLog</a> \u2014 session reports for vibe-coding developers</p>
176
+ </div>`;
177
+ }
178
+ /**
179
+ * Analyze git history and send the email report
180
+ */
181
+ async analyzeAndSend(options = {}) {
182
+ const report = await this.analyze(options);
183
+ if (report.totalCommits === 0) {
184
+ throw new Error("No commits found in the specified time range");
185
+ }
186
+ const html = this.buildEmail(report);
187
+ const subject = `${this.config.brandName}: ${report.totalCommits} commits \u2014 ${report.features.length} features, ${report.fixes.length} fixes`;
188
+ const { data, error } = await this.resend.emails.send({
189
+ from: this.config.fromEmail,
190
+ to: this.config.email,
191
+ cc: this.config.cc,
192
+ subject,
193
+ html
194
+ });
195
+ if (error) throw new Error(`Email send failed: ${error.message}`);
196
+ return { report, emailId: data?.id || "sent" };
197
+ }
198
+ /**
199
+ * Generate a LinkedIn post draft from the session report
200
+ */
201
+ generateLinkedInDraft(report) {
202
+ const topFeatures = report.features.slice(0, 3).map((f) => `\u2192 ${f}`).join("\n");
203
+ return `Shipped ${report.totalCommits} commits today.
204
+
205
+ ${report.features.length} features. ${report.fixes.length} fixes. ${report.duration} of building.
206
+
207
+ ${topFeatures}
208
+
209
+ The best developers don't just code \u2014 they ship. And they communicate what they shipped.
210
+
211
+ What did you build today?
212
+
213
+ #BuildInPublic #ShipLog #FounderLife`;
214
+ }
215
+ };
216
+ function escapeHtml(str) {
217
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
218
+ }
219
+
220
+ // src/cli.ts
221
+ var import_fs = require("fs");
222
+ var import_path = require("path");
223
+ var program = new import_commander.Command();
224
+ function loadConfig() {
225
+ const configPath = (0, import_path.join)(process.cwd(), ".shiplog.json");
226
+ if ((0, import_fs.existsSync)(configPath)) {
227
+ try {
228
+ return JSON.parse((0, import_fs.readFileSync)(configPath, "utf8"));
229
+ } catch {
230
+ return {};
231
+ }
232
+ }
233
+ return {};
234
+ }
235
+ program.name("shiplog").description("Session reports for vibe-coding developers").version("0.1.0");
236
+ program.command("send").description("Analyze recent git commits and send an email report").option("-e, --email <email>", "Email address to send report to").option("-s, --since <time>", "Time range (e.g. '4 hours ago', '1 day ago')", "8 hours ago").option("-b, --branch <branch>", "Git branch to analyze", "HEAD").option("-k, --key <apiKey>", "Resend API key").option("--brand <name>", "Brand name for email header").option("--color <hex>", "Brand color (hex)").option("--cc <emails>", "CC recipients (comma-separated)").option("--linkedin", "Include LinkedIn post draft").option("--dry-run", "Analyze only, don't send email").action(async (options) => {
237
+ const config = loadConfig();
238
+ const email = options.email || config.email || process.env.SHIPLOG_EMAIL;
239
+ const apiKey = options.key || config.resendApiKey || process.env.RESEND_API_KEY;
240
+ if (!email) {
241
+ console.error(import_chalk.default.red("Error: --email is required (or set in .shiplog.json / SHIPLOG_EMAIL env)"));
242
+ process.exit(1);
243
+ }
244
+ if (!apiKey && !options.dryRun) {
245
+ console.error(import_chalk.default.red("Error: --key is required (or set in .shiplog.json / RESEND_API_KEY env)"));
246
+ process.exit(1);
247
+ }
248
+ const spinner = (0, import_ora.default)("Analyzing git history...").start();
249
+ try {
250
+ const shiplog = new ShipLog({
251
+ email,
252
+ resendApiKey: apiKey || "",
253
+ brandName: options.brand || config.brandName || "ShipLog",
254
+ brandColor: options.color || config.brandColor || "#0077B5",
255
+ fromEmail: config.fromEmail,
256
+ cc: options.cc ? options.cc.split(",").map((e) => e.trim()) : config.cc,
257
+ includeLinkedInDraft: options.linkedin || config.includeLinkedInDraft
258
+ });
259
+ const report = await shiplog.analyze({ since: options.since, branch: options.branch });
260
+ spinner.succeed(import_chalk.default.green(`Found ${report.totalCommits} commits in ${report.duration}`));
261
+ console.log("");
262
+ console.log(import_chalk.default.bold("\u{1F4CA} Session Summary"));
263
+ console.log(import_chalk.default.gray("\u2500".repeat(40)));
264
+ console.log(` ${import_chalk.default.green("\u2728 Features:")} ${report.features.length}`);
265
+ report.features.forEach((f) => console.log(` ${import_chalk.default.green("\u2192")} ${f}`));
266
+ console.log(` ${import_chalk.default.red("\u{1F527} Fixes:")} ${report.fixes.length}`);
267
+ report.fixes.forEach((f) => console.log(` ${import_chalk.default.red("\u2192")} ${f}`));
268
+ console.log(` ${import_chalk.default.blue("\u267B\uFE0F Refactors:")} ${report.refactors.length}`);
269
+ console.log(` ${import_chalk.default.gray("\u{1F4DD} Other:")} ${report.other.length}`);
270
+ console.log(` ${import_chalk.default.cyan("\u{1F4C4} Lines:")} +${report.insertions} / -${report.deletions}`);
271
+ console.log("");
272
+ if (report.linkedInDraft) {
273
+ console.log(import_chalk.default.bold("\u{1F4BC} LinkedIn Draft:"));
274
+ console.log(import_chalk.default.gray("\u2500".repeat(40)));
275
+ console.log(import_chalk.default.dim(report.linkedInDraft));
276
+ console.log("");
277
+ }
278
+ if (options.dryRun) {
279
+ console.log(import_chalk.default.yellow("Dry run \u2014 email not sent."));
280
+ return;
281
+ }
282
+ const sendSpinner = (0, import_ora.default)(`Sending report to ${email}...`).start();
283
+ const result = await shiplog.analyzeAndSend({ since: options.since, branch: options.branch });
284
+ sendSpinner.succeed(import_chalk.default.green(`Email sent! (${result.emailId})`));
285
+ } catch (err) {
286
+ spinner.fail(import_chalk.default.red(err.message));
287
+ process.exit(1);
288
+ }
289
+ });
290
+ program.command("init").description("Create a .shiplog.json config file").action(() => {
291
+ const configPath = (0, import_path.join)(process.cwd(), ".shiplog.json");
292
+ if ((0, import_fs.existsSync)(configPath)) {
293
+ console.log(import_chalk.default.yellow(".shiplog.json already exists"));
294
+ return;
295
+ }
296
+ const template = JSON.stringify({
297
+ email: "you@example.com",
298
+ resendApiKey: "re_...",
299
+ brandName: "Your Project",
300
+ brandColor: "#0077B5",
301
+ includeLinkedInDraft: true
302
+ }, null, 2);
303
+ require("fs").writeFileSync(configPath, template);
304
+ console.log(import_chalk.default.green("Created .shiplog.json \u2014 edit it with your settings"));
305
+ });
306
+ program.parse();
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node