@kennethsolomon/shipkit 3.4.0 → 3.6.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.
@@ -0,0 +1,203 @@
1
+ // ShipKit Dashboard — zero-dependency Node.js server
2
+ const http = require("http");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execSync } = require("child_process");
6
+
7
+ const PORT =
8
+ parseInt(process.argv.find((_, i, a) => a[i - 1] === "--port") || process.env.PORT, 10) || 3333;
9
+
10
+ const HARD_GATES = new Set([12, 14, 16, 20, 22]);
11
+ const OPTIONALS = new Set([4, 5, 8, 18, 27]);
12
+
13
+ function stripMd(s) {
14
+ return (s || "").replace(/\*\*/g, "").replace(/`/g, "").trim();
15
+ }
16
+
17
+ function discoverWorktrees() {
18
+ try {
19
+ const raw = execSync("git worktree list", { encoding: "utf8" });
20
+ return raw
21
+ .trim()
22
+ .split("\n")
23
+ .filter(Boolean)
24
+ .map((line) => {
25
+ const branchMatch = line.match(/^(.+?)\s+[0-9a-f]+\s+\[(.+?)\]$/);
26
+ if (branchMatch) return { path: branchMatch[1].trim(), branch: branchMatch[2].trim() };
27
+ const detachedMatch = line.match(/^(.+?)\s+[0-9a-f]+\s+\((.+?)\)$/);
28
+ if (detachedMatch) return { path: detachedMatch[1].trim(), branch: detachedMatch[2].trim() };
29
+ return null;
30
+ })
31
+ .filter(Boolean);
32
+ } catch {
33
+ return [{ path: process.cwd(), branch: "unknown" }];
34
+ }
35
+ }
36
+
37
+ function parseWorkflowStatus(worktreePath) {
38
+ const filePath = path.join(worktreePath, "tasks", "workflow-status.md");
39
+ try {
40
+ const lines = fs.readFileSync(filePath, "utf8").split("\n");
41
+
42
+ let headerFound = false;
43
+ let separatorSkipped = false;
44
+ const steps = [];
45
+
46
+ for (const line of lines) {
47
+ if (!headerFound) {
48
+ if (line.includes("| # |")) headerFound = true;
49
+ continue;
50
+ }
51
+ if (!separatorSkipped) {
52
+ separatorSkipped = true;
53
+ continue;
54
+ }
55
+ const cells = line.split("|").slice(1, -1).map((c) => c.trim());
56
+ if (cells.length < 3) continue;
57
+
58
+ const number = parseInt(cells[0], 10);
59
+ if (isNaN(number)) continue;
60
+
61
+ const rawStep = stripMd(cells[1]);
62
+ const cmdMatch = rawStep.match(/\((.+?)\)/);
63
+ const command = cmdMatch ? cmdMatch[1].trim() : "";
64
+ const name = rawStep.replace(/\s*\(.+?\)\s*/, "").trim();
65
+
66
+ steps.push({
67
+ number,
68
+ name,
69
+ command,
70
+ status: stripMd(cells[2]),
71
+ notes: stripMd(cells[3]),
72
+ isHardGate: HARD_GATES.has(number),
73
+ isOptional: OPTIONALS.has(number),
74
+ });
75
+ }
76
+ return steps;
77
+ } catch (err) {
78
+ if (err.code === "ENOENT") return [];
79
+ process.stderr.write(`Error parsing workflow-status.md: ${err.message}\n`);
80
+ return [];
81
+ }
82
+ }
83
+
84
+ const STOP_HEADERS = new Set(["Verification", "Acceptance Criteria", "Risks", "Change Log", "Summary"]);
85
+
86
+ function parseTodo(worktreePath) {
87
+ const filePath = path.join(worktreePath, "tasks", "todo.md");
88
+ try {
89
+ const lines = fs.readFileSync(filePath, "utf8").split("\n");
90
+
91
+ let taskName = "";
92
+ let done = 0;
93
+ let total = 0;
94
+ let section = "";
95
+ let inMilestones = false;
96
+ let pastMilestones = false;
97
+ const todoItems = [];
98
+
99
+ for (const line of lines) {
100
+ if (!taskName && line.startsWith("# TODO")) {
101
+ const dashIdx = line.indexOf("—");
102
+ if (dashIdx !== -1) taskName = line.slice(dashIdx + 1).trim();
103
+ else taskName = line.replace(/^#\s*TODO\s*/, "").trim();
104
+ }
105
+
106
+ if (line.startsWith("## ")) {
107
+ const header = line.slice(3).trim();
108
+ if (header.startsWith("Milestone")) {
109
+ inMilestones = true;
110
+ section = header;
111
+ } else if (inMilestones && STOP_HEADERS.has(header)) {
112
+ pastMilestones = true;
113
+ }
114
+ }
115
+
116
+ if (/^\s*-\s*\[x\]/i.test(line)) {
117
+ done++;
118
+ total++;
119
+ if (inMilestones && !pastMilestones) {
120
+ todoItems.push({ text: stripMd(line.replace(/^\s*-\s*\[x\]\s*/i, "")), done: true, section });
121
+ }
122
+ } else if (/^\s*-\s*\[\s\]/.test(line)) {
123
+ total++;
124
+ if (inMilestones && !pastMilestones) {
125
+ todoItems.push({ text: stripMd(line.replace(/^\s*-\s*\[\s\]\s*/, "")), done: false, section });
126
+ }
127
+ }
128
+ }
129
+ return { taskName, todosDone: done, todosTotal: total, todoItems };
130
+ } catch (err) {
131
+ if (err.code === "ENOENT") return { taskName: "", todosDone: 0, todosTotal: 0, todoItems: [] };
132
+ process.stderr.write(`Error parsing todo.md: ${err.message}\n`);
133
+ return { taskName: "", todosDone: 0, todosTotal: 0, todoItems: [] };
134
+ }
135
+ }
136
+
137
+ function buildStatus() {
138
+ const worktrees = discoverWorktrees();
139
+ return worktrees.map((wt) => {
140
+ const steps = parseWorkflowStatus(wt.path);
141
+ const todo = parseTodo(wt.path);
142
+
143
+ let currentStep = 0;
144
+ let totalDone = 0;
145
+ let totalSkipped = 0;
146
+ for (const s of steps) {
147
+ if (s.status === ">> next <<") currentStep = s.number;
148
+ if (s.status === "done") totalDone++;
149
+ if (s.status === "skipped") totalSkipped++;
150
+ }
151
+
152
+ return {
153
+ path: wt.path,
154
+ branch: wt.branch,
155
+ taskName: todo.taskName,
156
+ todosDone: todo.todosDone,
157
+ todosTotal: todo.todosTotal,
158
+ todoItems: todo.todoItems,
159
+ currentStep,
160
+ totalDone,
161
+ totalSkipped,
162
+ totalSteps: steps.length,
163
+ steps,
164
+ };
165
+ });
166
+ }
167
+
168
+ const server = http.createServer((req, res) => {
169
+ res.setHeader("Access-Control-Allow-Origin", "*");
170
+
171
+ if (req.method === "GET" && req.url === "/") {
172
+ const htmlPath = path.join(__dirname, "dashboard.html");
173
+ try {
174
+ const html = fs.readFileSync(htmlPath, "utf8");
175
+ res.writeHead(200, { "Content-Type": "text/html" });
176
+ res.end(html);
177
+ } catch {
178
+ res.writeHead(404, { "Content-Type": "text/plain" });
179
+ res.end("dashboard.html not found");
180
+ }
181
+ return;
182
+ }
183
+
184
+ if (req.method === "GET" && req.url === "/api/status") {
185
+ try {
186
+ const data = buildStatus();
187
+ res.writeHead(200, { "Content-Type": "application/json" });
188
+ res.end(JSON.stringify(data));
189
+ } catch (err) {
190
+ process.stderr.write(`Error building status: ${err.message}\n`);
191
+ res.writeHead(500, { "Content-Type": "application/json" });
192
+ res.end(JSON.stringify({ error: "Internal server error" }));
193
+ }
194
+ return;
195
+ }
196
+
197
+ res.writeHead(404, { "Content-Type": "text/plain" });
198
+ res.end("Not found");
199
+ });
200
+
201
+ server.listen(PORT, () => {
202
+ console.log(`ShipKit Dashboard running at http://localhost:${PORT}`);
203
+ });
@@ -215,7 +215,84 @@ Maximum 3 loop iterations. If issues persist after 3 loops, present remaining is
215
215
 
216
216
  ---
217
217
 
218
- ## Step 9 — Present the Output
218
+ ## Step 9 — Generate Project Context Docs
219
+
220
+ Generate 3 lightweight documentation files in `docs/` from information already gathered in Steps 1-2. No new questions — use the product name, value prop, audience, features, and tech stack already captured.
221
+
222
+ ### Files to Generate
223
+
224
+ **`docs/vision.md`**
225
+ ```markdown
226
+ # [Product Name]
227
+
228
+ ## Value Proposition
229
+ [One-line value prop from Step 1]
230
+
231
+ ## Target Audience
232
+ [Target audience from Step 1]
233
+
234
+ ## Key Features
235
+ [Bullet list of 3-5 features from Step 1]
236
+
237
+ ## North Star Metric
238
+ [Suggest one metric that measures core value — e.g., "weekly active waitlist signups" or "daily feature usage"]
239
+ ```
240
+
241
+ **`docs/prd.md`**
242
+ ```markdown
243
+ # PRD — [Product Name]
244
+
245
+ ## Overview
246
+ [1-2 sentence product description]
247
+
248
+ ## User Stories
249
+ [For each key feature from Step 1, write a user story: "As a [audience], I want to [feature] so that [benefit]"]
250
+
251
+ ## Feature Acceptance Criteria
252
+ [For each feature, list 2-3 concrete acceptance criteria]
253
+
254
+ ## Out of Scope (MVP)
255
+ - Real authentication
256
+ - Real database
257
+ - Third-party integrations
258
+ - Deployment
259
+ ```
260
+
261
+ **`docs/tech-design.md`**
262
+ ```markdown
263
+ # Tech Design — [Product Name]
264
+
265
+ ## Stack
266
+ - **Framework:** [chosen stack from Step 2]
267
+ - **Styling:** Tailwind CSS
268
+ - **Fonts:** [chosen fonts]
269
+
270
+ ## Project Structure
271
+ [List the key directories and files generated during scaffolding]
272
+
273
+ ## Component Map
274
+ ### Landing Page
275
+ [List all 9 sections and their components]
276
+
277
+ ### App Pages
278
+ [List each page and its key components]
279
+
280
+ ## Data Model
281
+ ### Waitlist
282
+ - email: string (validated)
283
+ - timestamp: ISO 8601 string
284
+
285
+ ### Fake Data Entities
286
+ [List the fake data structures used in the app]
287
+ ```
288
+
289
+ After generating the docs, output:
290
+ > **Context docs generated:** `docs/vision.md`, `docs/prd.md`, `docs/tech-design.md`
291
+ > These persist context for future sessions. Run `/sk:context` to load `docs/vision.md` into the session brief, or read the others directly.
292
+
293
+ ---
294
+
295
+ ## Step 10 — Present the Output
219
296
 
220
297
  Summarize what was generated:
221
298
 
@@ -363,4 +363,5 @@ Create entries in: `[ARCH_CHANGELOG_DIR]`
363
363
  | `/sk:features` | Sync feature specs with shipped implementation |
364
364
  | `/sk:release` | Version bump + changelog + tag |
365
365
  | `/sk:status` | Show workflow + task status |
366
+ | `/sk:context` | Load all context files + output session brief for fast session start |
366
367
  | `/sk:setup-optimizer` | Diagnose + update workflow + enrich CLAUDE.md |