@whenlabs/when 0.10.0 → 0.11.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/action.yml +64 -2
- package/dist/chunk-JI5NCJQ2.js +496 -0
- package/dist/index.js +346 -8
- package/dist/mcp.js +10 -122
- package/package.json +6 -1
- package/dist/chunk-JOMP6AU5.js +0 -40
package/action.yml
CHANGED
|
@@ -1,17 +1,79 @@
|
|
|
1
1
|
name: 'WhenLabs CI Check'
|
|
2
2
|
description: 'Run documentation drift, env validation, and license checks'
|
|
3
|
+
branding:
|
|
4
|
+
icon: 'check-circle'
|
|
5
|
+
color: 'orange'
|
|
3
6
|
inputs:
|
|
4
7
|
tools:
|
|
5
|
-
description: 'Comma-separated tools to run'
|
|
8
|
+
description: 'Comma-separated tools to run (stale, envalid, vow)'
|
|
6
9
|
default: 'stale,envalid,vow'
|
|
10
|
+
checks:
|
|
11
|
+
description: 'Alias for tools — select which checks to run (stale, envalid, vow)'
|
|
12
|
+
default: ''
|
|
7
13
|
fail_on:
|
|
8
14
|
description: 'Fail on: error, warning, or none'
|
|
9
15
|
default: 'error'
|
|
16
|
+
comment:
|
|
17
|
+
description: 'Post results as a PR comment when running on a pull request'
|
|
18
|
+
default: 'false'
|
|
10
19
|
runs:
|
|
11
20
|
using: 'composite'
|
|
12
21
|
steps:
|
|
13
22
|
- uses: actions/setup-node@v4
|
|
14
23
|
with:
|
|
15
24
|
node-version: '20'
|
|
16
|
-
|
|
25
|
+
|
|
26
|
+
- name: Cache npm
|
|
27
|
+
uses: actions/cache@v4
|
|
28
|
+
with:
|
|
29
|
+
path: ~/.npm
|
|
30
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
31
|
+
restore-keys: |
|
|
32
|
+
${{ runner.os }}-node-
|
|
33
|
+
|
|
34
|
+
- name: Run WhenLabs checks
|
|
35
|
+
id: whenlabs_check
|
|
36
|
+
run: |
|
|
37
|
+
SELECTED="${{ inputs.checks != '' && inputs.checks || inputs.tools }}"
|
|
38
|
+
WHENLABS_TOOLS="$SELECTED" npx @whenlabs/when ci --ci 2>&1 | tee /tmp/whenlabs-output.txt
|
|
39
|
+
echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
|
|
40
|
+
shell: bash
|
|
41
|
+
|
|
42
|
+
- name: Post PR comment
|
|
43
|
+
if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }}
|
|
44
|
+
uses: actions/github-script@v7
|
|
45
|
+
with:
|
|
46
|
+
script: |
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const output = fs.readFileSync('/tmp/whenlabs-output.txt', 'utf8');
|
|
49
|
+
const exitCode = '${{ steps.whenlabs_check.outputs.exit_code }}';
|
|
50
|
+
const status = exitCode === '0' ? '✅ All checks passed' : '❌ Issues found';
|
|
51
|
+
const body = [
|
|
52
|
+
'## WhenLabs CI Check',
|
|
53
|
+
'',
|
|
54
|
+
status,
|
|
55
|
+
'',
|
|
56
|
+
'<details><summary>Full output</summary>',
|
|
57
|
+
'',
|
|
58
|
+
'```',
|
|
59
|
+
output.trim(),
|
|
60
|
+
'```',
|
|
61
|
+
'',
|
|
62
|
+
'</details>',
|
|
63
|
+
].join('\n');
|
|
64
|
+
await github.rest.issues.createComment({
|
|
65
|
+
owner: context.repo.owner,
|
|
66
|
+
repo: context.repo.repo,
|
|
67
|
+
issue_number: context.issue.number,
|
|
68
|
+
body,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
- name: Fail if issues found
|
|
72
|
+
if: ${{ inputs.fail_on != 'none' }}
|
|
73
|
+
run: |
|
|
74
|
+
EXIT="${{ steps.whenlabs_check.outputs.exit_code }}"
|
|
75
|
+
if [ "$EXIT" != "0" ]; then
|
|
76
|
+
echo "WhenLabs checks found issues. Set fail_on: none to suppress this failure."
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
17
79
|
shell: bash
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp/velocity-dashboard.ts
|
|
4
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { TaskQueries, initDb } from "@whenlabs/velocity-mcp/lib";
|
|
9
|
+
var DASH_DIR = join(homedir(), ".whenlabs");
|
|
10
|
+
var DASH_PATH = join(DASH_DIR, "dashboard.html");
|
|
11
|
+
function parseTags(raw) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
} catch {
|
|
15
|
+
return raw ? raw.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function fmtDuration(seconds) {
|
|
19
|
+
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
20
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
21
|
+
const h = Math.floor(seconds / 3600);
|
|
22
|
+
const m = Math.round(seconds % 3600 / 60);
|
|
23
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
24
|
+
}
|
|
25
|
+
function buildCategoryStats(tasks) {
|
|
26
|
+
const map = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const t of tasks) {
|
|
28
|
+
if (!t.duration_seconds) continue;
|
|
29
|
+
const existing = map.get(t.category) ?? { count: 0, total: 0 };
|
|
30
|
+
existing.count++;
|
|
31
|
+
existing.total += t.duration_seconds;
|
|
32
|
+
map.set(t.category, existing);
|
|
33
|
+
}
|
|
34
|
+
return Array.from(map.entries()).map(([category, { count, total }]) => ({
|
|
35
|
+
category,
|
|
36
|
+
count,
|
|
37
|
+
avgDuration: total / count,
|
|
38
|
+
totalDuration: total
|
|
39
|
+
})).sort((a, b) => b.count - a.count);
|
|
40
|
+
}
|
|
41
|
+
function buildTagCounts(tasks) {
|
|
42
|
+
const map = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const t of tasks) {
|
|
44
|
+
for (const tag of parseTags(t.tags)) {
|
|
45
|
+
map.set(tag, (map.get(tag) ?? 0) + 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Array.from(map.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count).slice(0, 15);
|
|
49
|
+
}
|
|
50
|
+
function svgBarChart(data, opts) {
|
|
51
|
+
const { width = 520, barHeight = 32, color = "#6366f1", valueFormatter = String } = opts;
|
|
52
|
+
if (data.length === 0) return '<p style="color:#888;font-size:13px">No data yet.</p>';
|
|
53
|
+
const maxVal = Math.max(...data.map((d) => d.value), 1);
|
|
54
|
+
const labelWidth = 120;
|
|
55
|
+
const valueWidth = 70;
|
|
56
|
+
const chartWidth = width - labelWidth - valueWidth - 16;
|
|
57
|
+
const height = data.length * (barHeight + 8) + 16;
|
|
58
|
+
const bars = data.map((d, i) => {
|
|
59
|
+
const barW = Math.max(2, d.value / maxVal * chartWidth);
|
|
60
|
+
const y = i * (barHeight + 8) + 8;
|
|
61
|
+
const sub = d.subtitle ? `<text x="${labelWidth + barW + 6}" y="${y + barHeight / 2 + 4}" font-size="10" fill="#999">${d.subtitle}</text>` : "";
|
|
62
|
+
return `
|
|
63
|
+
<text x="${labelWidth - 6}" y="${y + barHeight / 2 + 4}" text-anchor="end" font-size="12" fill="#ddd" font-family="system-ui,sans-serif">${d.label}</text>
|
|
64
|
+
<rect x="${labelWidth}" y="${y}" width="${barW}" height="${barHeight}" rx="4" fill="${color}" opacity="0.85"/>
|
|
65
|
+
<text x="${labelWidth + barW + 6}" y="${y + barHeight / 2 + 4}" font-size="12" fill="#e2e8f0" font-family="system-ui,sans-serif">${valueFormatter(d.value)}${sub ? "" : ""}</text>
|
|
66
|
+
${sub}`;
|
|
67
|
+
}).join("");
|
|
68
|
+
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
|
|
69
|
+
}
|
|
70
|
+
function generateHtml(tasks) {
|
|
71
|
+
const now = /* @__PURE__ */ new Date();
|
|
72
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
73
|
+
const recentTasks = tasks.filter((t) => t.ended_at && new Date(t.ended_at) >= sevenDaysAgo).sort((a, b) => new Date(b.ended_at).getTime() - new Date(a.ended_at).getTime()).slice(0, 20);
|
|
74
|
+
const catStats = buildCategoryStats(tasks);
|
|
75
|
+
const tagCounts = buildTagCounts(tasks);
|
|
76
|
+
const totalTasks = tasks.length;
|
|
77
|
+
const totalSeconds = tasks.reduce((sum, t) => sum + (t.duration_seconds ?? 0), 0);
|
|
78
|
+
const avgSeconds = totalTasks > 0 ? totalSeconds / totalTasks : 0;
|
|
79
|
+
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
|
80
|
+
const catChartData = catStats.map((c) => ({
|
|
81
|
+
label: c.category,
|
|
82
|
+
value: c.count,
|
|
83
|
+
subtitle: fmtDuration(c.avgDuration) + " avg"
|
|
84
|
+
}));
|
|
85
|
+
const durationChartData = catStats.filter((c) => c.avgDuration > 0).map((c) => ({
|
|
86
|
+
label: c.category,
|
|
87
|
+
value: Math.round(c.avgDuration),
|
|
88
|
+
subtitle: ""
|
|
89
|
+
}));
|
|
90
|
+
const tagChartData = tagCounts.map((t) => ({
|
|
91
|
+
label: t.tag,
|
|
92
|
+
value: t.count
|
|
93
|
+
}));
|
|
94
|
+
const catChart = svgBarChart(catChartData, {
|
|
95
|
+
color: "#6366f1",
|
|
96
|
+
valueFormatter: (v) => `${v} tasks`
|
|
97
|
+
});
|
|
98
|
+
const durationChart = svgBarChart(durationChartData, {
|
|
99
|
+
color: "#10b981",
|
|
100
|
+
valueFormatter: fmtDuration
|
|
101
|
+
});
|
|
102
|
+
const tagChart = svgBarChart(tagChartData, {
|
|
103
|
+
color: "#f59e0b",
|
|
104
|
+
valueFormatter: (v) => `${v}`
|
|
105
|
+
});
|
|
106
|
+
const recentRows = recentTasks.map((t) => {
|
|
107
|
+
const tags = parseTags(t.tags);
|
|
108
|
+
const dur = t.duration_seconds ? fmtDuration(t.duration_seconds) : "\u2014";
|
|
109
|
+
const statusColor = t.status === "completed" ? "#10b981" : t.status === "failed" ? "#ef4444" : "#94a3b8";
|
|
110
|
+
const tagBadges = tags.slice(0, 4).map((tag) => `<span class="tag">${tag}</span>`).join("");
|
|
111
|
+
return `
|
|
112
|
+
<tr>
|
|
113
|
+
<td><span class="cat-badge cat-${t.category}">${t.category}</span></td>
|
|
114
|
+
<td class="desc">${t.description}</td>
|
|
115
|
+
<td>${tagBadges}</td>
|
|
116
|
+
<td style="color:${statusColor};text-align:center">${t.status ?? "\u2014"}</td>
|
|
117
|
+
<td style="text-align:right;color:#94a3b8">${dur}</td>
|
|
118
|
+
</tr>`;
|
|
119
|
+
}).join("");
|
|
120
|
+
return `<!DOCTYPE html>
|
|
121
|
+
<html lang="en">
|
|
122
|
+
<head>
|
|
123
|
+
<meta charset="UTF-8">
|
|
124
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
125
|
+
<title>Velocity Dashboard</title>
|
|
126
|
+
<style>
|
|
127
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
128
|
+
body {
|
|
129
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
130
|
+
background: #0f172a;
|
|
131
|
+
color: #e2e8f0;
|
|
132
|
+
min-height: 100vh;
|
|
133
|
+
padding: 24px;
|
|
134
|
+
}
|
|
135
|
+
.header {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
margin-bottom: 28px;
|
|
140
|
+
border-bottom: 1px solid #1e293b;
|
|
141
|
+
padding-bottom: 16px;
|
|
142
|
+
}
|
|
143
|
+
.header h1 { font-size: 22px; font-weight: 700; color: #f1f5f9; }
|
|
144
|
+
.header .subtitle { font-size: 12px; color: #64748b; margin-top: 2px; }
|
|
145
|
+
.badge-when {
|
|
146
|
+
background: #6366f1;
|
|
147
|
+
color: #fff;
|
|
148
|
+
font-size: 11px;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
padding: 3px 10px;
|
|
151
|
+
border-radius: 999px;
|
|
152
|
+
}
|
|
153
|
+
.stats-grid {
|
|
154
|
+
display: grid;
|
|
155
|
+
grid-template-columns: repeat(4, 1fr);
|
|
156
|
+
gap: 14px;
|
|
157
|
+
margin-bottom: 28px;
|
|
158
|
+
}
|
|
159
|
+
@media (max-width: 700px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
160
|
+
.stat-card {
|
|
161
|
+
background: #1e293b;
|
|
162
|
+
border: 1px solid #334155;
|
|
163
|
+
border-radius: 10px;
|
|
164
|
+
padding: 16px 20px;
|
|
165
|
+
}
|
|
166
|
+
.stat-card .label { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: .05em; }
|
|
167
|
+
.stat-card .value { font-size: 28px; font-weight: 700; color: #f1f5f9; margin: 4px 0 2px; }
|
|
168
|
+
.stat-card .sub { font-size: 11px; color: #64748b; }
|
|
169
|
+
.charts-grid {
|
|
170
|
+
display: grid;
|
|
171
|
+
grid-template-columns: 1fr 1fr;
|
|
172
|
+
gap: 18px;
|
|
173
|
+
margin-bottom: 28px;
|
|
174
|
+
}
|
|
175
|
+
@media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }
|
|
176
|
+
.chart-card {
|
|
177
|
+
background: #1e293b;
|
|
178
|
+
border: 1px solid #334155;
|
|
179
|
+
border-radius: 10px;
|
|
180
|
+
padding: 18px 20px;
|
|
181
|
+
}
|
|
182
|
+
.chart-card.full { grid-column: 1 / -1; }
|
|
183
|
+
.chart-card h2 { font-size: 13px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 14px; }
|
|
184
|
+
.table-card {
|
|
185
|
+
background: #1e293b;
|
|
186
|
+
border: 1px solid #334155;
|
|
187
|
+
border-radius: 10px;
|
|
188
|
+
padding: 18px 20px;
|
|
189
|
+
overflow-x: auto;
|
|
190
|
+
}
|
|
191
|
+
.table-card h2 { font-size: 13px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 14px; }
|
|
192
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
193
|
+
th { text-align: left; color: #64748b; font-weight: 500; font-size: 11px; text-transform: uppercase; padding: 6px 10px; border-bottom: 1px solid #334155; }
|
|
194
|
+
td { padding: 8px 10px; border-bottom: 1px solid #1e293b; vertical-align: middle; }
|
|
195
|
+
tr:last-child td { border-bottom: none; }
|
|
196
|
+
.desc { max-width: 340px; color: #cbd5e1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
197
|
+
.tag {
|
|
198
|
+
display: inline-block;
|
|
199
|
+
background: #334155;
|
|
200
|
+
color: #94a3b8;
|
|
201
|
+
font-size: 10px;
|
|
202
|
+
padding: 1px 7px;
|
|
203
|
+
border-radius: 999px;
|
|
204
|
+
margin-right: 3px;
|
|
205
|
+
}
|
|
206
|
+
.cat-badge {
|
|
207
|
+
display: inline-block;
|
|
208
|
+
font-size: 10px;
|
|
209
|
+
font-weight: 600;
|
|
210
|
+
padding: 2px 8px;
|
|
211
|
+
border-radius: 999px;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
letter-spacing: .03em;
|
|
214
|
+
}
|
|
215
|
+
.cat-scaffold { background:#1e3a5f; color:#60a5fa; }
|
|
216
|
+
.cat-implement { background:#1a2e3b; color:#38bdf8; }
|
|
217
|
+
.cat-refactor { background:#1e3b2e; color:#4ade80; }
|
|
218
|
+
.cat-debug { background:#3b2020; color:#f87171; }
|
|
219
|
+
.cat-test { background:#2d2b3b; color:#a78bfa; }
|
|
220
|
+
.cat-config { background:#2d2a1a; color:#fbbf24; }
|
|
221
|
+
.cat-docs { background:#1e2e3b; color:#7dd3fc; }
|
|
222
|
+
.cat-deploy { background:#1e2e1e; color:#86efac; }
|
|
223
|
+
.generated-at { font-size: 11px; color: #475569; margin-top: 20px; text-align: right; }
|
|
224
|
+
</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body>
|
|
227
|
+
<div class="header">
|
|
228
|
+
<div>
|
|
229
|
+
<h1>Velocity Dashboard</h1>
|
|
230
|
+
<div class="subtitle">Generated ${now.toLocaleString()}</div>
|
|
231
|
+
</div>
|
|
232
|
+
<span class="badge-when">@whenlabs/when</span>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div class="stats-grid">
|
|
236
|
+
<div class="stat-card">
|
|
237
|
+
<div class="label">Total Tasks</div>
|
|
238
|
+
<div class="value">${totalTasks}</div>
|
|
239
|
+
<div class="sub">${completedCount} completed</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="stat-card">
|
|
242
|
+
<div class="label">Total Time</div>
|
|
243
|
+
<div class="value">${fmtDuration(totalSeconds)}</div>
|
|
244
|
+
<div class="sub">across all tasks</div>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="stat-card">
|
|
247
|
+
<div class="label">Avg Duration</div>
|
|
248
|
+
<div class="value">${fmtDuration(avgSeconds)}</div>
|
|
249
|
+
<div class="sub">per task</div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="stat-card">
|
|
252
|
+
<div class="label">Categories</div>
|
|
253
|
+
<div class="value">${catStats.length}</div>
|
|
254
|
+
<div class="sub">active categories</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="charts-grid">
|
|
259
|
+
<div class="chart-card">
|
|
260
|
+
<h2>Tasks by Category</h2>
|
|
261
|
+
${catChart}
|
|
262
|
+
</div>
|
|
263
|
+
<div class="chart-card">
|
|
264
|
+
<h2>Avg Duration by Category</h2>
|
|
265
|
+
${durationChart}
|
|
266
|
+
</div>
|
|
267
|
+
<div class="chart-card full">
|
|
268
|
+
<h2>Top Tags</h2>
|
|
269
|
+
${tagChart}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="table-card">
|
|
274
|
+
<h2>Recent Tasks (last 7 days)</h2>
|
|
275
|
+
${recentRows ? `<table>
|
|
276
|
+
<thead><tr>
|
|
277
|
+
<th>Category</th><th>Description</th><th>Tags</th><th style="text-align:center">Status</th><th style="text-align:right">Duration</th>
|
|
278
|
+
</tr></thead>
|
|
279
|
+
<tbody>${recentRows}</tbody>
|
|
280
|
+
</table>` : '<p style="color:#888;font-size:13px">No tasks in the last 7 days.</p>'}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div class="generated-at">Generated by <code>when velocity dashboard</code></div>
|
|
284
|
+
</body>
|
|
285
|
+
</html>`;
|
|
286
|
+
}
|
|
287
|
+
async function generateDashboard() {
|
|
288
|
+
const db = initDb();
|
|
289
|
+
const queries = new TaskQueries(db);
|
|
290
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
291
|
+
const yearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3).toISOString();
|
|
292
|
+
const tasks = queries.getCompletedInRange(yearAgo, now);
|
|
293
|
+
db.close();
|
|
294
|
+
const html = generateHtml(tasks);
|
|
295
|
+
mkdirSync(DASH_DIR, { recursive: true });
|
|
296
|
+
writeFileSync(DASH_PATH, html, "utf8");
|
|
297
|
+
const catStats = buildCategoryStats(tasks);
|
|
298
|
+
const totalSeconds = tasks.reduce((sum, t) => sum + (t.duration_seconds ?? 0), 0);
|
|
299
|
+
const topCats = catStats.slice(0, 3).map((c) => `${c.category}(${c.count})`).join(", ");
|
|
300
|
+
const summary = [
|
|
301
|
+
`Total tasks: ${tasks.length}`,
|
|
302
|
+
`Total time: ${fmtDuration(totalSeconds)}`,
|
|
303
|
+
`Top categories: ${topCats || "none yet"}`,
|
|
304
|
+
`Dashboard written to: ${DASH_PATH}`
|
|
305
|
+
].join("\n");
|
|
306
|
+
return { path: DASH_PATH, summary };
|
|
307
|
+
}
|
|
308
|
+
function openFile(filePath) {
|
|
309
|
+
const platform = process.platform;
|
|
310
|
+
try {
|
|
311
|
+
if (platform === "darwin") {
|
|
312
|
+
execSync(`open "${filePath}"`, { stdio: "ignore" });
|
|
313
|
+
} else if (platform === "linux") {
|
|
314
|
+
execSync(`xdg-open "${filePath}"`, { stdio: "ignore" });
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function registerVelocityDashboard(server) {
|
|
320
|
+
server.tool(
|
|
321
|
+
"velocity_dashboard",
|
|
322
|
+
"Generate an HTML dashboard with charts showing task timing stats \u2014 opens in browser",
|
|
323
|
+
{},
|
|
324
|
+
async () => {
|
|
325
|
+
const { path, summary } = await generateDashboard();
|
|
326
|
+
openFile(path);
|
|
327
|
+
const text = `Dashboard generated and opened in browser.
|
|
328
|
+
|
|
329
|
+
${summary}`;
|
|
330
|
+
return { content: [{ type: "text", text }] };
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/utils/find-bin.ts
|
|
336
|
+
import { resolve, dirname } from "path";
|
|
337
|
+
import { existsSync } from "fs";
|
|
338
|
+
import { fileURLToPath } from "url";
|
|
339
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
340
|
+
function findBin(name) {
|
|
341
|
+
const pkgRoot = resolve(__dirname, "../..");
|
|
342
|
+
const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
|
|
343
|
+
if (existsSync(localBin)) return localBin;
|
|
344
|
+
const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
|
|
345
|
+
if (existsSync(directCli)) return directCli;
|
|
346
|
+
return name;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/config/whenlabs-config.ts
|
|
350
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
351
|
+
import { resolve as resolve2 } from "path";
|
|
352
|
+
import { parse } from "yaml";
|
|
353
|
+
var CONFIG_FILENAME = ".whenlabs.yml";
|
|
354
|
+
function loadConfig(projectPath) {
|
|
355
|
+
const dir = projectPath ?? process.cwd();
|
|
356
|
+
const configPath = resolve2(dir, CONFIG_FILENAME);
|
|
357
|
+
if (!existsSync2(configPath)) return null;
|
|
358
|
+
try {
|
|
359
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
360
|
+
const parsed = parse(raw);
|
|
361
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
362
|
+
return parsed;
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/mcp/run-cli.ts
|
|
369
|
+
import { spawn } from "child_process";
|
|
370
|
+
import { join as join2 } from "path";
|
|
371
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
372
|
+
import { homedir as homedir2 } from "os";
|
|
373
|
+
function runCli(bin, args, cwd) {
|
|
374
|
+
return new Promise((res) => {
|
|
375
|
+
const child = spawn(findBin(bin), args, {
|
|
376
|
+
cwd: cwd || process.cwd(),
|
|
377
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
|
|
378
|
+
});
|
|
379
|
+
let stdout = "";
|
|
380
|
+
let stderr = "";
|
|
381
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
382
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
383
|
+
child.on("error", (err) => res({ stdout, stderr: err.message, code: 1 }));
|
|
384
|
+
child.on("close", (code) => res({ stdout, stderr, code: code ?? 0 }));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
var CACHE_DIR = join2(homedir2(), ".whenlabs", "cache");
|
|
388
|
+
function writeCache(tool, project, output, code) {
|
|
389
|
+
try {
|
|
390
|
+
mkdirSync2(CACHE_DIR, { recursive: true });
|
|
391
|
+
const file = join2(CACHE_DIR, `${tool}_${project}.json`);
|
|
392
|
+
writeFileSync2(file, JSON.stringify({ timestamp: Date.now(), output, code }));
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function deriveProject(path) {
|
|
397
|
+
const dir = path || process.cwd();
|
|
398
|
+
return dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "unknown";
|
|
399
|
+
}
|
|
400
|
+
function readAwareProjectName(path) {
|
|
401
|
+
try {
|
|
402
|
+
const awareFile = join2(path || process.cwd(), ".aware.json");
|
|
403
|
+
if (!existsSync3(awareFile)) return null;
|
|
404
|
+
const data = JSON.parse(readFileSync2(awareFile, "utf8"));
|
|
405
|
+
return data.name || data.project || null;
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function formatOutput(result) {
|
|
411
|
+
const parts = [];
|
|
412
|
+
if (result.stdout.trim()) parts.push(result.stdout.trim());
|
|
413
|
+
if (result.stderr.trim()) parts.push(result.stderr.trim());
|
|
414
|
+
return parts.join("\n") || "No output";
|
|
415
|
+
}
|
|
416
|
+
async function checkTriggers(toolName, result, path) {
|
|
417
|
+
const output = result.stdout || result.stderr || "";
|
|
418
|
+
const extras = [];
|
|
419
|
+
if (toolName === "aware_init") {
|
|
420
|
+
const madeChanges = /wrote|created|updated|generated/i.test(output);
|
|
421
|
+
if (madeChanges) {
|
|
422
|
+
const staleResult = await runCli("stale", ["scan"], path);
|
|
423
|
+
const staleOutput = staleResult.stdout || staleResult.stderr || "";
|
|
424
|
+
writeCache("stale", deriveProject(path), staleOutput, staleResult.code);
|
|
425
|
+
if (staleOutput.trim()) {
|
|
426
|
+
extras.push(`
|
|
427
|
+
--- Auto-triggered stale_scan (stack change detected) ---
|
|
428
|
+
${staleOutput}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (toolName === "vow_scan") {
|
|
433
|
+
const hasUnknown = /unknown|UNKNOWN|unlicensed/i.test(output);
|
|
434
|
+
if (hasUnknown) {
|
|
435
|
+
extras.push("\nNote: Unknown licenses detected \u2014 check README for license accuracy claims.");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (toolName === "berth_check") {
|
|
439
|
+
const hasConflicts = /conflict|in use|occupied|taken/i.test(output);
|
|
440
|
+
if (hasConflicts) {
|
|
441
|
+
const projectName = readAwareProjectName(path);
|
|
442
|
+
if (projectName) {
|
|
443
|
+
extras.push(`
|
|
444
|
+
Note: Conflicts found in project "${projectName}".`);
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const cacheFiles = readdirSync(CACHE_DIR).filter((f) => f.startsWith("stale_"));
|
|
448
|
+
for (const cacheFile of cacheFiles) {
|
|
449
|
+
const cached = JSON.parse(readFileSync2(join2(CACHE_DIR, cacheFile), "utf8"));
|
|
450
|
+
if (/\b\d{4,5}\b/.test(cached.output || "")) {
|
|
451
|
+
extras.push("\nTip: Port references found in documentation \u2014 stale_scan may need re-run after resolving conflicts.");
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (toolName === "envalid_detect") {
|
|
460
|
+
const serviceUrlMatches = output.match(/\b[A-Z_]*(?:HOST|PORT|URL|URI)[A-Z_]*\b/g);
|
|
461
|
+
if (serviceUrlMatches && serviceUrlMatches.length > 0) {
|
|
462
|
+
const examples = [...new Set(serviceUrlMatches)].slice(0, 3).join(", ");
|
|
463
|
+
extras.push(`
|
|
464
|
+
Tip: Service URLs detected (${examples}, etc.) \u2014 run berth_register to track their ports for conflict detection.`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (toolName === "velocity_end_task") {
|
|
468
|
+
const largeChange = /actual_files["\s:]+([1-9]\d)/i.test(output) || /\b([6-9]|\d{2,})\s+files?\b/i.test(output);
|
|
469
|
+
if (largeChange) {
|
|
470
|
+
extras.push("\nTip: Large change detected \u2014 consider running stale_scan to check for documentation drift.");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (toolName === "vow_scan") {
|
|
474
|
+
const cacheFile = join2(CACHE_DIR, `vow_scan_${deriveProject(path)}.json`);
|
|
475
|
+
const isFirstScan = !existsSync3(cacheFile);
|
|
476
|
+
const hasNewPackages = /new package|added|installed/i.test(output);
|
|
477
|
+
if (isFirstScan || hasNewPackages) {
|
|
478
|
+
extras.push("\nTip: Dependency changes detected \u2014 run aware_sync to update AI context files with new library info.");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return extras;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export {
|
|
485
|
+
findBin,
|
|
486
|
+
CONFIG_FILENAME,
|
|
487
|
+
loadConfig,
|
|
488
|
+
runCli,
|
|
489
|
+
CACHE_DIR,
|
|
490
|
+
writeCache,
|
|
491
|
+
deriveProject,
|
|
492
|
+
formatOutput,
|
|
493
|
+
checkTriggers,
|
|
494
|
+
generateDashboard,
|
|
495
|
+
registerVelocityDashboard
|
|
496
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CACHE_DIR,
|
|
3
4
|
CONFIG_FILENAME,
|
|
5
|
+
deriveProject,
|
|
4
6
|
findBin,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
generateDashboard,
|
|
8
|
+
loadConfig,
|
|
9
|
+
runCli,
|
|
10
|
+
writeCache
|
|
11
|
+
} from "./chunk-JI5NCJQ2.js";
|
|
7
12
|
import {
|
|
8
13
|
getStatusPath
|
|
9
14
|
} from "./chunk-4ZVSCJCJ.js";
|
|
10
15
|
|
|
11
16
|
// src/index.ts
|
|
12
|
-
import { Command as
|
|
17
|
+
import { Command as Command10 } from "commander";
|
|
13
18
|
|
|
14
19
|
// src/commands/delegate.ts
|
|
15
20
|
import { Command } from "commander";
|
|
@@ -684,7 +689,7 @@ function writeStatus(results) {
|
|
|
684
689
|
writeFileSync2(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
|
|
685
690
|
}
|
|
686
691
|
function sleep(ms) {
|
|
687
|
-
return new Promise((
|
|
692
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
688
693
|
}
|
|
689
694
|
function createWatchCommand() {
|
|
690
695
|
const cmd = new Command4("watch");
|
|
@@ -928,13 +933,341 @@ function createConfigCommand() {
|
|
|
928
933
|
return cmd;
|
|
929
934
|
}
|
|
930
935
|
|
|
931
|
-
// src/
|
|
936
|
+
// src/commands/upgrade.ts
|
|
937
|
+
import { Command as Command6 } from "commander";
|
|
938
|
+
import { execSync } from "child_process";
|
|
932
939
|
import { readFileSync as readFileSync3 } from "fs";
|
|
933
940
|
import { resolve as resolve4, dirname as dirname3 } from "path";
|
|
934
941
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
935
942
|
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
936
|
-
var
|
|
937
|
-
|
|
943
|
+
var c4 = {
|
|
944
|
+
reset: "\x1B[0m",
|
|
945
|
+
bold: "\x1B[1m",
|
|
946
|
+
green: "\x1B[32m",
|
|
947
|
+
yellow: "\x1B[33m",
|
|
948
|
+
red: "\x1B[31m",
|
|
949
|
+
cyan: "\x1B[36m",
|
|
950
|
+
dim: "\x1B[2m"
|
|
951
|
+
};
|
|
952
|
+
function colorize4(text, ...codes) {
|
|
953
|
+
return codes.join("") + text + c4.reset;
|
|
954
|
+
}
|
|
955
|
+
function parseVersion(v) {
|
|
956
|
+
return v.trim().split(".").map(Number);
|
|
957
|
+
}
|
|
958
|
+
function versionGte(a, b) {
|
|
959
|
+
const pa = parseVersion(a);
|
|
960
|
+
const pb = parseVersion(b);
|
|
961
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
962
|
+
const na = pa[i] ?? 0;
|
|
963
|
+
const nb = pb[i] ?? 0;
|
|
964
|
+
if (na > nb) return true;
|
|
965
|
+
if (na < nb) return false;
|
|
966
|
+
}
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
function createUpgradeCommand() {
|
|
970
|
+
const cmd = new Command6("upgrade");
|
|
971
|
+
cmd.description("Upgrade @whenlabs/when to the latest version");
|
|
972
|
+
cmd.action(async () => {
|
|
973
|
+
console.log("");
|
|
974
|
+
console.log(colorize4(" when upgrade", c4.bold, c4.cyan));
|
|
975
|
+
console.log(colorize4(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c4.dim));
|
|
976
|
+
const pkgPath = resolve4(__dirname3, "..", "..", "package.json");
|
|
977
|
+
let current;
|
|
978
|
+
try {
|
|
979
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
980
|
+
current = pkg.version;
|
|
981
|
+
} catch {
|
|
982
|
+
console.log(` ${colorize4("!", c4.red)} Could not read current version`);
|
|
983
|
+
console.log("");
|
|
984
|
+
process.exitCode = 1;
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
console.log(` ${colorize4("current", c4.dim)} ${colorize4(current, c4.bold)}`);
|
|
988
|
+
let latest;
|
|
989
|
+
try {
|
|
990
|
+
latest = execSync("npm view @whenlabs/when version", { encoding: "utf-8" }).trim();
|
|
991
|
+
} catch {
|
|
992
|
+
console.log(` ${colorize4("!", c4.yellow)} Could not reach npm registry \u2014 check your network connection`);
|
|
993
|
+
console.log("");
|
|
994
|
+
process.exitCode = 1;
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
console.log(` ${colorize4("latest", c4.dim)} ${colorize4(latest, c4.bold)}`);
|
|
998
|
+
console.log("");
|
|
999
|
+
if (versionGte(current, latest)) {
|
|
1000
|
+
console.log(` ${colorize4("\u2713", c4.green)} Already up to date`);
|
|
1001
|
+
console.log("");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
console.log(` ${colorize4("\u2191", c4.yellow)} Upgrade available: ${colorize4(current, c4.dim)} \u2192 ${colorize4(latest, c4.green + c4.bold)}`);
|
|
1005
|
+
console.log(` ${colorize4("\u2022", c4.dim)} Running: ${colorize4("npm install -g @whenlabs/when@latest", c4.bold)}`);
|
|
1006
|
+
console.log("");
|
|
1007
|
+
try {
|
|
1008
|
+
execSync("npm install -g @whenlabs/when@latest", { stdio: "inherit" });
|
|
1009
|
+
console.log("");
|
|
1010
|
+
console.log(` ${colorize4("\u2713", c4.green)} Upgraded to ${colorize4(latest, c4.bold)}`);
|
|
1011
|
+
} catch {
|
|
1012
|
+
console.log(` ${colorize4("\u2717", c4.red)} Install failed \u2014 try running with sudo or check npm permissions`);
|
|
1013
|
+
process.exitCode = 1;
|
|
1014
|
+
}
|
|
1015
|
+
console.log("");
|
|
1016
|
+
});
|
|
1017
|
+
return cmd;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/commands/eject.ts
|
|
1021
|
+
import { Command as Command7 } from "commander";
|
|
1022
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync4, copyFileSync } from "fs";
|
|
1023
|
+
import { resolve as resolve5 } from "path";
|
|
1024
|
+
import { stringify as stringify3 } from "yaml";
|
|
1025
|
+
var c5 = {
|
|
1026
|
+
reset: "\x1B[0m",
|
|
1027
|
+
bold: "\x1B[1m",
|
|
1028
|
+
green: "\x1B[32m",
|
|
1029
|
+
yellow: "\x1B[33m",
|
|
1030
|
+
red: "\x1B[31m",
|
|
1031
|
+
cyan: "\x1B[36m",
|
|
1032
|
+
dim: "\x1B[2m"
|
|
1033
|
+
};
|
|
1034
|
+
function colorize5(text, ...codes) {
|
|
1035
|
+
return codes.join("") + text + c5.reset;
|
|
1036
|
+
}
|
|
1037
|
+
function createEjectCommand() {
|
|
1038
|
+
const cmd = new Command7("eject");
|
|
1039
|
+
cmd.description("Write each tool section of .whenlabs.yml back to its native config format");
|
|
1040
|
+
cmd.option("--force", "Overwrite existing files without prompting");
|
|
1041
|
+
cmd.action((options) => {
|
|
1042
|
+
const cwd = process.cwd();
|
|
1043
|
+
console.log("");
|
|
1044
|
+
console.log(colorize5(" when eject", c5.bold, c5.cyan));
|
|
1045
|
+
console.log(colorize5(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c5.dim));
|
|
1046
|
+
const config = loadConfig(cwd);
|
|
1047
|
+
if (!config) {
|
|
1048
|
+
console.log(` ${colorize5("!", c5.yellow)} No ${colorize5(".whenlabs.yml", c5.bold)} found \u2014 nothing to eject`);
|
|
1049
|
+
console.log(` ${colorize5("\u2022", c5.dim)} Run ${colorize5("when config init", c5.bold)} to generate one first`);
|
|
1050
|
+
console.log("");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
let ejected = 0;
|
|
1054
|
+
let skipped = 0;
|
|
1055
|
+
if (config.stale && Object.keys(config.stale).length > 0) {
|
|
1056
|
+
const dest = resolve5(cwd, ".stale.yml");
|
|
1057
|
+
if (existsSync4(dest) && !options.force) {
|
|
1058
|
+
console.log(` ${colorize5("!", c5.yellow)} ${colorize5(".stale.yml", c5.bold)} already exists \u2014 use ${colorize5("--force", c5.bold)} to overwrite`);
|
|
1059
|
+
skipped++;
|
|
1060
|
+
} else {
|
|
1061
|
+
const yaml = stringify3(config.stale, { lineWidth: 0 });
|
|
1062
|
+
writeFileSync4(dest, yaml, "utf-8");
|
|
1063
|
+
console.log(` ${colorize5("+", c5.green)} Wrote ${colorize5(".stale.yml", c5.bold)}`);
|
|
1064
|
+
ejected++;
|
|
1065
|
+
}
|
|
1066
|
+
} else if (config.stale !== void 0) {
|
|
1067
|
+
console.log(` ${colorize5("-", c5.dim)} stale: empty config \u2014 skipping .stale.yml`);
|
|
1068
|
+
}
|
|
1069
|
+
if (config.vow && Object.keys(config.vow).length > 0) {
|
|
1070
|
+
const dest = resolve5(cwd, ".vow.json");
|
|
1071
|
+
if (existsSync4(dest) && !options.force) {
|
|
1072
|
+
console.log(` ${colorize5("!", c5.yellow)} ${colorize5(".vow.json", c5.bold)} already exists \u2014 use ${colorize5("--force", c5.bold)} to overwrite`);
|
|
1073
|
+
skipped++;
|
|
1074
|
+
} else {
|
|
1075
|
+
writeFileSync4(dest, JSON.stringify(config.vow, null, 2) + "\n", "utf-8");
|
|
1076
|
+
console.log(` ${colorize5("+", c5.green)} Wrote ${colorize5(".vow.json", c5.bold)}`);
|
|
1077
|
+
ejected++;
|
|
1078
|
+
}
|
|
1079
|
+
} else if (config.vow !== void 0) {
|
|
1080
|
+
console.log(` ${colorize5("-", c5.dim)} vow: empty config \u2014 skipping .vow.json`);
|
|
1081
|
+
}
|
|
1082
|
+
if (config.envalid?.schema) {
|
|
1083
|
+
const src = resolve5(cwd, config.envalid.schema);
|
|
1084
|
+
const dest = resolve5(cwd, ".env.schema");
|
|
1085
|
+
const isSamePath = resolve5(src) === resolve5(dest);
|
|
1086
|
+
if (isSamePath) {
|
|
1087
|
+
console.log(` ${colorize5("-", c5.dim)} envalid.schema already points to ${colorize5(".env.schema", c5.bold)}`);
|
|
1088
|
+
} else if (!existsSync4(src)) {
|
|
1089
|
+
console.log(` ${colorize5("!", c5.yellow)} envalid.schema source ${colorize5(config.envalid.schema, c5.bold)} not found \u2014 skipping`);
|
|
1090
|
+
skipped++;
|
|
1091
|
+
} else if (existsSync4(dest) && !options.force) {
|
|
1092
|
+
console.log(` ${colorize5("!", c5.yellow)} ${colorize5(".env.schema", c5.bold)} already exists \u2014 use ${colorize5("--force", c5.bold)} to overwrite`);
|
|
1093
|
+
skipped++;
|
|
1094
|
+
} else {
|
|
1095
|
+
copyFileSync(src, dest);
|
|
1096
|
+
console.log(` ${colorize5("+", c5.green)} Copied ${colorize5(config.envalid.schema, c5.bold)} \u2192 ${colorize5(".env.schema", c5.bold)}`);
|
|
1097
|
+
ejected++;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (config.berth !== void 0) {
|
|
1101
|
+
const portCount = config.berth.ports ? Object.keys(config.berth.ports).length : 0;
|
|
1102
|
+
if (portCount > 0) {
|
|
1103
|
+
console.log(` ${colorize5("\u2022", c5.dim)} berth: ${portCount} port(s) configured \u2014 berth has no standalone config file, managed via ${colorize5(".whenlabs.yml", c5.bold)}`);
|
|
1104
|
+
} else {
|
|
1105
|
+
console.log(` ${colorize5("-", c5.dim)} berth: no standalone config file`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
console.log("");
|
|
1109
|
+
if (ejected > 0) {
|
|
1110
|
+
console.log(` ${colorize5("\u2713", c5.green)} Ejected ${colorize5(String(ejected), c5.bold)} file(s)`);
|
|
1111
|
+
}
|
|
1112
|
+
if (skipped > 0) {
|
|
1113
|
+
console.log(` ${colorize5("!", c5.yellow)} Skipped ${colorize5(String(skipped), c5.bold)} file(s) \u2014 run with ${colorize5("--force", c5.bold)} to overwrite`);
|
|
1114
|
+
}
|
|
1115
|
+
if (ejected === 0 && skipped === 0) {
|
|
1116
|
+
console.log(` ${colorize5("-", c5.dim)} Nothing to eject`);
|
|
1117
|
+
}
|
|
1118
|
+
console.log("");
|
|
1119
|
+
});
|
|
1120
|
+
return cmd;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/commands/diff.ts
|
|
1124
|
+
import { Command as Command8 } from "commander";
|
|
1125
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1126
|
+
import { join as join2 } from "path";
|
|
1127
|
+
var c6 = {
|
|
1128
|
+
reset: "\x1B[0m",
|
|
1129
|
+
bold: "\x1B[1m",
|
|
1130
|
+
green: "\x1B[32m",
|
|
1131
|
+
yellow: "\x1B[33m",
|
|
1132
|
+
red: "\x1B[31m",
|
|
1133
|
+
cyan: "\x1B[36m",
|
|
1134
|
+
dim: "\x1B[2m"
|
|
1135
|
+
};
|
|
1136
|
+
function colorize6(text, ...codes) {
|
|
1137
|
+
return codes.join("") + text + c6.reset;
|
|
1138
|
+
}
|
|
1139
|
+
function readCache(tool, project) {
|
|
1140
|
+
const file = join2(CACHE_DIR, `${tool}_${project}.json`);
|
|
1141
|
+
if (!existsSync5(file)) return null;
|
|
1142
|
+
try {
|
|
1143
|
+
return JSON.parse(readFileSync5(file, "utf-8"));
|
|
1144
|
+
} catch {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function diffLines(oldOutput, newOutput) {
|
|
1149
|
+
const oldLines = new Set(oldOutput.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
1150
|
+
const newLines = new Set(newOutput.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
1151
|
+
const added = [];
|
|
1152
|
+
const removed = [];
|
|
1153
|
+
const unchanged = [];
|
|
1154
|
+
for (const line of newLines) {
|
|
1155
|
+
if (oldLines.has(line)) {
|
|
1156
|
+
unchanged.push(line);
|
|
1157
|
+
} else {
|
|
1158
|
+
added.push(line);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
for (const line of oldLines) {
|
|
1162
|
+
if (!newLines.has(line)) {
|
|
1163
|
+
removed.push(line);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return { added, removed, unchanged };
|
|
1167
|
+
}
|
|
1168
|
+
var TOOLS = [
|
|
1169
|
+
{ bin: "stale", args: ["scan"], label: "stale" },
|
|
1170
|
+
{ bin: "envalid", args: ["validate"], label: "envalid" },
|
|
1171
|
+
{ bin: "berth", args: ["status"], label: "berth" },
|
|
1172
|
+
{ bin: "vow", args: ["scan"], label: "vow" },
|
|
1173
|
+
{ bin: "aware", args: ["doctor"], label: "aware" }
|
|
1174
|
+
];
|
|
1175
|
+
function createDiffCommand() {
|
|
1176
|
+
const cmd = new Command8("diff");
|
|
1177
|
+
cmd.description("Compare cached tool results to fresh runs and show what changed");
|
|
1178
|
+
cmd.action(async () => {
|
|
1179
|
+
const cwd = process.cwd();
|
|
1180
|
+
const project = deriveProject(cwd);
|
|
1181
|
+
console.log("");
|
|
1182
|
+
console.log(colorize6(" when diff", c6.bold, c6.cyan));
|
|
1183
|
+
console.log(colorize6(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c6.dim));
|
|
1184
|
+
console.log(` ${colorize6("project", c6.dim)} ${colorize6(project, c6.bold)}`);
|
|
1185
|
+
console.log("");
|
|
1186
|
+
let anyChanges = false;
|
|
1187
|
+
for (const tool of TOOLS) {
|
|
1188
|
+
const cached = readCache(tool.label, project);
|
|
1189
|
+
const fresh = await runCli(tool.bin, tool.args, cwd);
|
|
1190
|
+
const freshOutput = fresh.stdout.trim() || fresh.stderr.trim() || "";
|
|
1191
|
+
if (!cached) {
|
|
1192
|
+
console.log(` ${colorize6(tool.label, c6.bold, c6.cyan)}`);
|
|
1193
|
+
if (freshOutput) {
|
|
1194
|
+
for (const line of freshOutput.split("\n").slice(0, 5)) {
|
|
1195
|
+
if (line.trim()) console.log(` ${colorize6(line, c6.dim)}`);
|
|
1196
|
+
}
|
|
1197
|
+
const total = freshOutput.split("\n").filter(Boolean).length;
|
|
1198
|
+
if (total > 5) console.log(` ${colorize6(`\u2026 ${total - 5} more lines`, c6.dim)}`);
|
|
1199
|
+
} else {
|
|
1200
|
+
console.log(` ${colorize6("no output", c6.dim)}`);
|
|
1201
|
+
}
|
|
1202
|
+
console.log(` ${colorize6("(no prior cache \u2014 this is now the baseline)", c6.dim)}`);
|
|
1203
|
+
} else {
|
|
1204
|
+
const oldOutput = cached.output.trim();
|
|
1205
|
+
const { added, removed, unchanged } = diffLines(oldOutput, freshOutput);
|
|
1206
|
+
const hasChanges = added.length > 0 || removed.length > 0;
|
|
1207
|
+
if (hasChanges) anyChanges = true;
|
|
1208
|
+
console.log(` ${colorize6(tool.label, c6.bold, c6.cyan)}`);
|
|
1209
|
+
if (!hasChanges) {
|
|
1210
|
+
console.log(` ${colorize6("\u2713", c6.dim)} ${colorize6("no changes", c6.dim)} ${colorize6(`(${unchanged.length} line(s))`, c6.dim)}`);
|
|
1211
|
+
} else {
|
|
1212
|
+
for (const line of added) {
|
|
1213
|
+
console.log(` ${colorize6("+", c6.green)} ${colorize6(line, c6.green)}`);
|
|
1214
|
+
}
|
|
1215
|
+
for (const line of removed) {
|
|
1216
|
+
console.log(` ${colorize6("-", c6.red)} ${colorize6(line, c6.red)}`);
|
|
1217
|
+
}
|
|
1218
|
+
if (unchanged.length > 0) {
|
|
1219
|
+
console.log(` ${colorize6("\xB7", c6.dim)} ${colorize6(`${unchanged.length} line(s) unchanged`, c6.dim)}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
writeCache(tool.label, project, freshOutput, fresh.code);
|
|
1224
|
+
console.log("");
|
|
1225
|
+
}
|
|
1226
|
+
if (!anyChanges) {
|
|
1227
|
+
console.log(` ${colorize6("\u2713", c6.green)} All tools unchanged since last run`);
|
|
1228
|
+
} else {
|
|
1229
|
+
console.log(` ${colorize6("\u2022", c6.dim)} Cache updated with latest results`);
|
|
1230
|
+
}
|
|
1231
|
+
console.log("");
|
|
1232
|
+
});
|
|
1233
|
+
return cmd;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/commands/dashboard.ts
|
|
1237
|
+
import { Command as Command9 } from "commander";
|
|
1238
|
+
import { execSync as execSync2 } from "child_process";
|
|
1239
|
+
function createDashboardCommand() {
|
|
1240
|
+
const cmd = new Command9("dashboard");
|
|
1241
|
+
cmd.description("Generate an HTML velocity dashboard and open it in the browser");
|
|
1242
|
+
cmd.option("--no-open", "Write the HTML file without opening the browser");
|
|
1243
|
+
cmd.action(async (options) => {
|
|
1244
|
+
const { path, summary } = await generateDashboard();
|
|
1245
|
+
console.log(summary);
|
|
1246
|
+
if (options.open !== false) {
|
|
1247
|
+
const platform = process.platform;
|
|
1248
|
+
try {
|
|
1249
|
+
if (platform === "darwin") {
|
|
1250
|
+
execSync2(`open "${path}"`, { stdio: "ignore" });
|
|
1251
|
+
} else if (platform === "linux") {
|
|
1252
|
+
execSync2(`xdg-open "${path}"`, { stdio: "ignore" });
|
|
1253
|
+
} else {
|
|
1254
|
+
console.log(`Dashboard written to: ${path}`);
|
|
1255
|
+
}
|
|
1256
|
+
} catch {
|
|
1257
|
+
console.log(`Dashboard written to: ${path}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
return cmd;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/index.ts
|
|
1265
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1266
|
+
import { resolve as resolve6, dirname as dirname4 } from "path";
|
|
1267
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1268
|
+
var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
|
|
1269
|
+
var { version } = JSON.parse(readFileSync6(resolve6(__dirname4, "..", "package.json"), "utf8"));
|
|
1270
|
+
var program = new Command10();
|
|
938
1271
|
program.name("when").version(version).description("The WhenLabs developer toolkit \u2014 6 tools, one install");
|
|
939
1272
|
program.command("install").description("Install all WhenLabs tools globally (MCP server + CLAUDE.md instructions)").option("--cursor", "Install MCP servers into Cursor (~/.cursor/mcp.json)").option("--vscode", "Install MCP servers into VS Code (settings.json)").option("--windsurf", "Install MCP servers into Windsurf (~/.codeium/windsurf/mcp_config.json)").option("--all", "Install MCP servers into all supported editors").action(async (options) => {
|
|
940
1273
|
const { install } = await import("./install-33GE3HKA.js");
|
|
@@ -956,10 +1289,15 @@ program.addCommand(createInitCommand());
|
|
|
956
1289
|
program.addCommand(createDoctorCommand());
|
|
957
1290
|
program.addCommand(createWatchCommand());
|
|
958
1291
|
program.addCommand(createConfigCommand());
|
|
1292
|
+
program.addCommand(createUpgradeCommand());
|
|
1293
|
+
program.addCommand(createEjectCommand());
|
|
1294
|
+
program.addCommand(createDiffCommand());
|
|
959
1295
|
program.addCommand(createDelegateCommand("stale", "Detect documentation drift in your codebase"));
|
|
960
1296
|
program.addCommand(createDelegateCommand("envalid", "Validate .env files against a type-safe schema"));
|
|
961
1297
|
program.addCommand(createDelegateCommand("berth", "Detect and resolve port conflicts"));
|
|
962
1298
|
program.addCommand(createDelegateCommand("aware", "Auto-detect your stack and generate AI context files"));
|
|
963
1299
|
program.addCommand(createDelegateCommand("vow", "Scan dependency licenses and validate against policies"));
|
|
964
|
-
|
|
1300
|
+
var velocityCmd = createDelegateCommand("velocity", "velocity-mcp task timing server", "velocity-mcp");
|
|
1301
|
+
velocityCmd.addCommand(createDashboardCommand());
|
|
1302
|
+
program.addCommand(velocityCmd);
|
|
965
1303
|
program.parse();
|
package/dist/mcp.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
checkTriggers,
|
|
4
|
+
deriveProject,
|
|
5
|
+
formatOutput,
|
|
6
|
+
registerVelocityDashboard,
|
|
7
|
+
runCli,
|
|
8
|
+
writeCache
|
|
9
|
+
} from "./chunk-JI5NCJQ2.js";
|
|
5
10
|
|
|
6
11
|
// src/mcp/index.ts
|
|
7
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
-
import { readFileSync
|
|
14
|
+
import { readFileSync } from "fs";
|
|
10
15
|
import { resolve, dirname } from "path";
|
|
11
16
|
import { fileURLToPath } from "url";
|
|
12
17
|
import {
|
|
@@ -21,124 +26,6 @@ import {
|
|
|
21
26
|
|
|
22
27
|
// src/mcp/stale.ts
|
|
23
28
|
import { z } from "zod";
|
|
24
|
-
|
|
25
|
-
// src/mcp/run-cli.ts
|
|
26
|
-
import { spawn } from "child_process";
|
|
27
|
-
import { join } from "path";
|
|
28
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
|
|
29
|
-
import { homedir } from "os";
|
|
30
|
-
function runCli(bin, args, cwd) {
|
|
31
|
-
return new Promise((res) => {
|
|
32
|
-
const child = spawn(findBin(bin), args, {
|
|
33
|
-
cwd: cwd || process.cwd(),
|
|
34
|
-
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
|
|
35
|
-
});
|
|
36
|
-
let stdout = "";
|
|
37
|
-
let stderr = "";
|
|
38
|
-
child.stdout.on("data", (d) => stdout += d);
|
|
39
|
-
child.stderr.on("data", (d) => stderr += d);
|
|
40
|
-
child.on("error", (err) => res({ stdout, stderr: err.message, code: 1 }));
|
|
41
|
-
child.on("close", (code) => res({ stdout, stderr, code: code ?? 0 }));
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
var CACHE_DIR = join(homedir(), ".whenlabs", "cache");
|
|
45
|
-
function writeCache(tool, project, output, code) {
|
|
46
|
-
try {
|
|
47
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
48
|
-
const file = join(CACHE_DIR, `${tool}_${project}.json`);
|
|
49
|
-
writeFileSync(file, JSON.stringify({ timestamp: Date.now(), output, code }));
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function deriveProject(path) {
|
|
54
|
-
const dir = path || process.cwd();
|
|
55
|
-
return dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "unknown";
|
|
56
|
-
}
|
|
57
|
-
function readAwareProjectName(path) {
|
|
58
|
-
try {
|
|
59
|
-
const awareFile = join(path || process.cwd(), ".aware.json");
|
|
60
|
-
if (!existsSync(awareFile)) return null;
|
|
61
|
-
const data = JSON.parse(readFileSync(awareFile, "utf8"));
|
|
62
|
-
return data.name || data.project || null;
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function formatOutput(result) {
|
|
68
|
-
const parts = [];
|
|
69
|
-
if (result.stdout.trim()) parts.push(result.stdout.trim());
|
|
70
|
-
if (result.stderr.trim()) parts.push(result.stderr.trim());
|
|
71
|
-
return parts.join("\n") || "No output";
|
|
72
|
-
}
|
|
73
|
-
async function checkTriggers(toolName, result, path) {
|
|
74
|
-
const output = result.stdout || result.stderr || "";
|
|
75
|
-
const extras = [];
|
|
76
|
-
if (toolName === "aware_init") {
|
|
77
|
-
const madeChanges = /wrote|created|updated|generated/i.test(output);
|
|
78
|
-
if (madeChanges) {
|
|
79
|
-
const staleResult = await runCli("stale", ["scan"], path);
|
|
80
|
-
const staleOutput = staleResult.stdout || staleResult.stderr || "";
|
|
81
|
-
writeCache("stale", deriveProject(path), staleOutput, staleResult.code);
|
|
82
|
-
if (staleOutput.trim()) {
|
|
83
|
-
extras.push(`
|
|
84
|
-
--- Auto-triggered stale_scan (stack change detected) ---
|
|
85
|
-
${staleOutput}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (toolName === "vow_scan") {
|
|
90
|
-
const hasUnknown = /unknown|UNKNOWN|unlicensed/i.test(output);
|
|
91
|
-
if (hasUnknown) {
|
|
92
|
-
extras.push("\nNote: Unknown licenses detected \u2014 check README for license accuracy claims.");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (toolName === "berth_check") {
|
|
96
|
-
const hasConflicts = /conflict|in use|occupied|taken/i.test(output);
|
|
97
|
-
if (hasConflicts) {
|
|
98
|
-
const projectName = readAwareProjectName(path);
|
|
99
|
-
if (projectName) {
|
|
100
|
-
extras.push(`
|
|
101
|
-
Note: Conflicts found in project "${projectName}".`);
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
const cacheFiles = readdirSync(CACHE_DIR).filter((f) => f.startsWith("stale_"));
|
|
105
|
-
for (const cacheFile of cacheFiles) {
|
|
106
|
-
const cached = JSON.parse(readFileSync(join(CACHE_DIR, cacheFile), "utf8"));
|
|
107
|
-
if (/\b\d{4,5}\b/.test(cached.output || "")) {
|
|
108
|
-
extras.push("\nTip: Port references found in documentation \u2014 stale_scan may need re-run after resolving conflicts.");
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (toolName === "envalid_detect") {
|
|
117
|
-
const serviceUrlMatches = output.match(/\b[A-Z_]*(?:HOST|PORT|URL|URI)[A-Z_]*\b/g);
|
|
118
|
-
if (serviceUrlMatches && serviceUrlMatches.length > 0) {
|
|
119
|
-
const examples = [...new Set(serviceUrlMatches)].slice(0, 3).join(", ");
|
|
120
|
-
extras.push(`
|
|
121
|
-
Tip: Service URLs detected (${examples}, etc.) \u2014 run berth_register to track their ports for conflict detection.`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (toolName === "velocity_end_task") {
|
|
125
|
-
const largeChange = /actual_files["\s:]+([1-9]\d)/i.test(output) || /\b([6-9]|\d{2,})\s+files?\b/i.test(output);
|
|
126
|
-
if (largeChange) {
|
|
127
|
-
extras.push("\nTip: Large change detected \u2014 consider running stale_scan to check for documentation drift.");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (toolName === "vow_scan") {
|
|
131
|
-
const cacheFile = join(CACHE_DIR, `vow_scan_${deriveProject(path)}.json`);
|
|
132
|
-
const isFirstScan = !existsSync(cacheFile);
|
|
133
|
-
const hasNewPackages = /new package|added|installed/i.test(output);
|
|
134
|
-
if (isFirstScan || hasNewPackages) {
|
|
135
|
-
extras.push("\nTip: Dependency changes detected \u2014 run aware_sync to update AI context files with new library info.");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return extras;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// src/mcp/stale.ts
|
|
142
29
|
function registerStaleTools(server2) {
|
|
143
30
|
server2.tool(
|
|
144
31
|
"stale_scan",
|
|
@@ -833,7 +720,7 @@ function registerVowTools(server2) {
|
|
|
833
720
|
|
|
834
721
|
// src/mcp/index.ts
|
|
835
722
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
836
|
-
var { version } = JSON.parse(
|
|
723
|
+
var { version } = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8"));
|
|
837
724
|
var server = new McpServer({
|
|
838
725
|
name: "whenlabs",
|
|
839
726
|
version
|
|
@@ -851,6 +738,7 @@ registerEnvalidTools(server);
|
|
|
851
738
|
registerBerthTools(server);
|
|
852
739
|
registerAwareTools(server);
|
|
853
740
|
registerVowTools(server);
|
|
741
|
+
registerVelocityDashboard(server);
|
|
854
742
|
process.on("SIGINT", () => {
|
|
855
743
|
velocityDb.close();
|
|
856
744
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whenlabs/when",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"mcpName": "io.github.whenlabs-org/when",
|
|
4
5
|
"description": "The WhenLabs developer toolkit — 6 tools, one install",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
"claude-code"
|
|
25
26
|
],
|
|
26
27
|
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/WhenLabs-org/when.git"
|
|
31
|
+
},
|
|
27
32
|
"engines": {
|
|
28
33
|
"node": ">=20"
|
|
29
34
|
},
|
package/dist/chunk-JOMP6AU5.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/utils/find-bin.ts
|
|
4
|
-
import { resolve, dirname } from "path";
|
|
5
|
-
import { existsSync } from "fs";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
function findBin(name) {
|
|
9
|
-
const pkgRoot = resolve(__dirname, "../..");
|
|
10
|
-
const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
|
|
11
|
-
if (existsSync(localBin)) return localBin;
|
|
12
|
-
const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
|
|
13
|
-
if (existsSync(directCli)) return directCli;
|
|
14
|
-
return name;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// src/config/whenlabs-config.ts
|
|
18
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
19
|
-
import { resolve as resolve2 } from "path";
|
|
20
|
-
import { parse } from "yaml";
|
|
21
|
-
var CONFIG_FILENAME = ".whenlabs.yml";
|
|
22
|
-
function loadConfig(projectPath) {
|
|
23
|
-
const dir = projectPath ?? process.cwd();
|
|
24
|
-
const configPath = resolve2(dir, CONFIG_FILENAME);
|
|
25
|
-
if (!existsSync2(configPath)) return null;
|
|
26
|
-
try {
|
|
27
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
28
|
-
const parsed = parse(raw);
|
|
29
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
30
|
-
return parsed;
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export {
|
|
37
|
-
findBin,
|
|
38
|
-
CONFIG_FILENAME,
|
|
39
|
-
loadConfig
|
|
40
|
-
};
|