@jtalk22/slack-mcp 3.0.0 → 3.2.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 +63 -28
- package/docs/SETUP.md +64 -29
- package/docs/TROUBLESHOOTING.md +28 -0
- package/lib/handlers.js +156 -0
- package/lib/slack-client.js +11 -3
- package/lib/token-store.js +6 -5
- package/lib/tools.js +131 -0
- package/package.json +35 -36
- package/public/index.html +10 -6
- package/public/share.html +128 -0
- package/scripts/setup-wizard.js +1 -1
- package/server.json +10 -4
- package/src/server-http.js +16 -1
- package/src/server.js +31 -7
- package/src/web-server.js +119 -4
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
- package/docs/COMMUNICATION-STYLE.md +0 -66
- package/docs/COMPATIBILITY.md +0 -19
- package/docs/DEPLOYMENT-MODES.md +0 -55
- package/docs/HN-LAUNCH.md +0 -72
- package/docs/INDEX.md +0 -40
- package/docs/INSTALL-PROOF.md +0 -18
- package/docs/LAUNCH-COPY-v3.0.0.md +0 -73
- package/docs/LAUNCH-MATRIX.md +0 -22
- package/docs/LAUNCH-OPS.md +0 -71
- package/docs/RELEASE-HEALTH.md +0 -90
- package/docs/SUPPORT-BOUNDARIES.md +0 -49
- package/docs/USE_CASE_RECIPES.md +0 -69
- package/docs/WEB-API.md +0 -303
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-claude-mobile-360x800.png +0 -0
- package/docs/images/demo-claude-mobile-390x844.png +0 -0
- package/docs/images/demo-main-mobile-360x800.png +0 -0
- package/docs/images/demo-main-mobile-390x844.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/diagram-oauth-comparison.svg +0 -80
- package/docs/images/diagram-session-flow.svg +0 -105
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/public/demo-claude.html +0 -1958
- package/public/demo-video.html +0 -235
- package/public/demo.html +0 -1196
- package/scripts/build-release-health-delta.js +0 -201
- package/scripts/capture-screenshots.js +0 -146
- package/scripts/check-owner-attribution.sh +0 -80
- package/scripts/check-public-language.sh +0 -25
- package/scripts/check-version-parity.js +0 -176
- package/scripts/cloudflare-browser-tool.js +0 -237
- package/scripts/collect-release-health.js +0 -150
- package/scripts/record-demo.js +0 -162
- package/scripts/release-preflight.js +0 -243
- package/scripts/setup-git-hooks.sh +0 -15
- package/scripts/verify-core.js +0 -159
- package/scripts/verify-install-flow.js +0 -193
- package/scripts/verify-web.js +0 -269
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, relative, resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
const METRICS = [
|
|
7
|
-
"npm downloads (last week)",
|
|
8
|
-
"npm downloads (last month)",
|
|
9
|
-
"stars",
|
|
10
|
-
"forks",
|
|
11
|
-
"open issues",
|
|
12
|
-
"14d views",
|
|
13
|
-
"14d unique visitors",
|
|
14
|
-
"14d clones",
|
|
15
|
-
"14d unique cloners",
|
|
16
|
-
"deployment-intake submissions (all-time)",
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const DEFAULT_AFTER = resolve("docs", "release-health", "latest.md");
|
|
20
|
-
const DEFAULT_OUT = resolve("docs", "release-health", "automation-delta.md");
|
|
21
|
-
const TARGETS = {
|
|
22
|
-
"npm downloads (last week)": { operator: ">=", value: 180 },
|
|
23
|
-
"deployment-intake submissions (all-time)": { operator: ">=", value: 2 },
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function escapeRegex(input) {
|
|
27
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function parseArgs(argv) {
|
|
31
|
-
const args = { before: null, after: DEFAULT_AFTER, out: DEFAULT_OUT };
|
|
32
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
-
const arg = argv[i];
|
|
34
|
-
if (arg === "--before") {
|
|
35
|
-
if (!argv[i + 1]) throw new Error("Missing value for --before");
|
|
36
|
-
args.before = argv[i + 1];
|
|
37
|
-
i += 1;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (arg === "--after") {
|
|
41
|
-
if (!argv[i + 1]) throw new Error("Missing value for --after");
|
|
42
|
-
args.after = argv[i + 1];
|
|
43
|
-
i += 1;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (arg === "--out") {
|
|
47
|
-
if (!argv[i + 1]) throw new Error("Missing value for --out");
|
|
48
|
-
args.out = argv[i + 1];
|
|
49
|
-
i += 1;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
53
|
-
}
|
|
54
|
-
return args;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readSnapshot(path) {
|
|
58
|
-
if (!path || !existsSync(path)) {
|
|
59
|
-
return { path, metrics: new Map(), generatedAt: null };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const content = readFileSync(path, "utf8");
|
|
63
|
-
const generatedAtMatch = content.match(/^- Generated:\s+(.+)$/m);
|
|
64
|
-
const metrics = new Map();
|
|
65
|
-
|
|
66
|
-
for (const label of METRICS) {
|
|
67
|
-
const metricRegex = new RegExp(`^- ${escapeRegex(label)}:\\s+(.+)$`, "m");
|
|
68
|
-
const match = content.match(metricRegex);
|
|
69
|
-
if (!match) continue;
|
|
70
|
-
|
|
71
|
-
const raw = match[1].trim();
|
|
72
|
-
if (raw.toLowerCase() === "n/a") {
|
|
73
|
-
metrics.set(label, null);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const parsed = Number(raw.replace(/,/g, ""));
|
|
78
|
-
metrics.set(label, Number.isFinite(parsed) ? parsed : null);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
path,
|
|
83
|
-
metrics,
|
|
84
|
-
generatedAt: generatedAtMatch ? generatedAtMatch[1].trim() : null,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function formatValue(value) {
|
|
89
|
-
return value === null || value === undefined ? "n/a" : `${value}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function formatDelta(beforeValue, afterValue) {
|
|
93
|
-
if (!Number.isFinite(beforeValue) || !Number.isFinite(afterValue)) {
|
|
94
|
-
return "n/a";
|
|
95
|
-
}
|
|
96
|
-
const delta = afterValue - beforeValue;
|
|
97
|
-
if (delta > 0) return `+${delta}`;
|
|
98
|
-
return `${delta}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function evaluateTarget(metricLabel, value) {
|
|
102
|
-
const target = TARGETS[metricLabel];
|
|
103
|
-
if (!target || !Number.isFinite(value)) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (target.operator === ">=") {
|
|
108
|
-
return value >= target.value;
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function displayPath(path) {
|
|
114
|
-
if (!path) return "(none found)";
|
|
115
|
-
const relPath = relative(process.cwd(), path);
|
|
116
|
-
if (relPath && !relPath.startsWith("..")) {
|
|
117
|
-
return relPath;
|
|
118
|
-
}
|
|
119
|
-
return path;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function renderMarkdown(beforeSnapshot, afterSnapshot, rows) {
|
|
123
|
-
const lines = [];
|
|
124
|
-
lines.push("# Automated Release Health Delta");
|
|
125
|
-
lines.push("");
|
|
126
|
-
lines.push(
|
|
127
|
-
`- Baseline snapshot: ${
|
|
128
|
-
beforeSnapshot.path && existsSync(beforeSnapshot.path) ? `\`${displayPath(beforeSnapshot.path)}\`` : "`(none found)`"
|
|
129
|
-
}`
|
|
130
|
-
);
|
|
131
|
-
lines.push(`- Current snapshot: \`${displayPath(afterSnapshot.path)}\``);
|
|
132
|
-
lines.push("");
|
|
133
|
-
lines.push("| Metric | Baseline | Current | Delta |");
|
|
134
|
-
lines.push("|---|---:|---:|---:|");
|
|
135
|
-
|
|
136
|
-
for (const row of rows) {
|
|
137
|
-
lines.push(
|
|
138
|
-
`| ${row.metric} | ${formatValue(row.beforeValue)} | ${formatValue(row.afterValue)} | ${formatDelta(
|
|
139
|
-
row.beforeValue,
|
|
140
|
-
row.afterValue
|
|
141
|
-
)} |`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
lines.push("");
|
|
146
|
-
lines.push("## Target Checks");
|
|
147
|
-
lines.push("");
|
|
148
|
-
|
|
149
|
-
for (const metric of Object.keys(TARGETS)) {
|
|
150
|
-
const value = afterSnapshot.metrics.get(metric) ?? null;
|
|
151
|
-
const passed = evaluateTarget(metric, value);
|
|
152
|
-
const target = TARGETS[metric];
|
|
153
|
-
const status = passed === null ? "n/a" : passed ? "pass" : "miss";
|
|
154
|
-
lines.push(`- ${metric} ${target.operator} ${target.value}: ${status} (current: ${formatValue(value)})`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (beforeSnapshot.generatedAt || afterSnapshot.generatedAt) {
|
|
158
|
-
lines.push("");
|
|
159
|
-
lines.push("## Snapshot Times");
|
|
160
|
-
lines.push("");
|
|
161
|
-
if (beforeSnapshot.generatedAt) {
|
|
162
|
-
lines.push(`- Baseline generated: ${beforeSnapshot.generatedAt}`);
|
|
163
|
-
}
|
|
164
|
-
if (afterSnapshot.generatedAt) {
|
|
165
|
-
lines.push(`- Current generated: ${afterSnapshot.generatedAt}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return `${lines.join("\n")}\n`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function main() {
|
|
173
|
-
const args = parseArgs(process.argv.slice(2));
|
|
174
|
-
const beforeSnapshot = readSnapshot(args.before ? resolve(args.before) : null);
|
|
175
|
-
const afterPath = resolve(args.after);
|
|
176
|
-
|
|
177
|
-
if (!existsSync(afterPath)) {
|
|
178
|
-
throw new Error(`Current snapshot not found: ${afterPath}`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const afterSnapshot = readSnapshot(afterPath);
|
|
182
|
-
const rows = METRICS.map((metric) => ({
|
|
183
|
-
metric,
|
|
184
|
-
beforeValue: beforeSnapshot.metrics.get(metric) ?? null,
|
|
185
|
-
afterValue: afterSnapshot.metrics.get(metric) ?? null,
|
|
186
|
-
}));
|
|
187
|
-
|
|
188
|
-
const outputPath = resolve(args.out);
|
|
189
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
190
|
-
const markdown = renderMarkdown(beforeSnapshot, afterSnapshot, rows);
|
|
191
|
-
writeFileSync(outputPath, markdown);
|
|
192
|
-
|
|
193
|
-
console.log(`Wrote ${outputPath}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
main();
|
|
198
|
-
} catch (error) {
|
|
199
|
-
console.error(error.message);
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Screenshot capture script using Playwright
|
|
4
|
-
* Captures desktop + mobile screenshots for README/docs
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { chromium } from 'playwright';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
import { dirname, join } from 'path';
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = dirname(__filename);
|
|
13
|
-
const projectRoot = join(__dirname, '..');
|
|
14
|
-
const imagesDir = join(projectRoot, 'docs', 'images');
|
|
15
|
-
|
|
16
|
-
const viewports = [
|
|
17
|
-
{ width: 390, height: 844, suffix: '390x844' },
|
|
18
|
-
{ width: 360, height: 800, suffix: '360x800' }
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
async function openPage(browser, filePath, viewport) {
|
|
22
|
-
const context = await browser.newContext({
|
|
23
|
-
viewport,
|
|
24
|
-
deviceScaleFactor: 2,
|
|
25
|
-
colorScheme: 'dark'
|
|
26
|
-
});
|
|
27
|
-
const page = await context.newPage();
|
|
28
|
-
await page.goto(`file://${filePath}`);
|
|
29
|
-
await page.waitForTimeout(1000);
|
|
30
|
-
return { context, page };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function captureScreenshots() {
|
|
34
|
-
console.log('Launching browser...');
|
|
35
|
-
|
|
36
|
-
const browser = await chromium.launch({
|
|
37
|
-
headless: true
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const demoPath = join(projectRoot, 'public', 'demo.html');
|
|
41
|
-
const demoClaudePath = join(projectRoot, 'public', 'demo-claude.html');
|
|
42
|
-
const indexPath = join(projectRoot, 'public', 'index.html');
|
|
43
|
-
|
|
44
|
-
// Desktop captures from demo.html
|
|
45
|
-
{
|
|
46
|
-
const { context, page } = await openPage(browser, demoPath, { width: 1400, height: 900 });
|
|
47
|
-
|
|
48
|
-
console.log('Capturing desktop demo screenshots...');
|
|
49
|
-
await page.screenshot({
|
|
50
|
-
path: join(imagesDir, 'demo-main.png'),
|
|
51
|
-
clip: { x: 0, y: 0, width: 1400, height: 800 }
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const mainPanel = await page.$('.main-panel');
|
|
55
|
-
if (mainPanel) {
|
|
56
|
-
await mainPanel.screenshot({
|
|
57
|
-
path: join(imagesDir, 'demo-messages.png')
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const sidebar = await page.$('.sidebar');
|
|
62
|
-
if (sidebar) {
|
|
63
|
-
await sidebar.screenshot({
|
|
64
|
-
path: join(imagesDir, 'demo-sidebar.png')
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await page.evaluate(() => runScenario('listChannels'));
|
|
69
|
-
await page.waitForTimeout(2600);
|
|
70
|
-
await page.screenshot({
|
|
71
|
-
path: join(imagesDir, 'demo-channels.png'),
|
|
72
|
-
clip: { x: 0, y: 0, width: 1400, height: 800 }
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await page.click('.conversation-item:first-child');
|
|
76
|
-
await page.waitForTimeout(600);
|
|
77
|
-
await page.screenshot({
|
|
78
|
-
path: join(imagesDir, 'demo-channel-messages.png'),
|
|
79
|
-
clip: { x: 0, y: 0, width: 1400, height: 800 }
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await context.close();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Poster from the Claude demo
|
|
86
|
-
{
|
|
87
|
-
const { context, page } = await openPage(browser, demoClaudePath, { width: 1280, height: 800 });
|
|
88
|
-
console.log('Capturing poster image...');
|
|
89
|
-
await page.screenshot({
|
|
90
|
-
path: join(imagesDir, 'demo-poster.png'),
|
|
91
|
-
clip: { x: 0, y: 0, width: 1280, height: 800 }
|
|
92
|
-
});
|
|
93
|
-
await context.close();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Mobile captures for web, demo, and claude demo pages
|
|
97
|
-
for (const viewport of viewports) {
|
|
98
|
-
const label = `${viewport.width}x${viewport.height}`;
|
|
99
|
-
console.log(`Capturing mobile screenshots (${label})...`);
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
const { context, page } = await openPage(browser, demoPath, viewport);
|
|
103
|
-
await page.screenshot({
|
|
104
|
-
path: join(imagesDir, `demo-main-mobile-${viewport.suffix}.png`),
|
|
105
|
-
fullPage: true
|
|
106
|
-
});
|
|
107
|
-
await context.close();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
const { context, page } = await openPage(browser, demoClaudePath, viewport);
|
|
112
|
-
await page.screenshot({
|
|
113
|
-
path: join(imagesDir, `demo-claude-mobile-${viewport.suffix}.png`),
|
|
114
|
-
fullPage: true
|
|
115
|
-
});
|
|
116
|
-
await context.close();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
const { context, page } = await openPage(browser, indexPath, viewport);
|
|
121
|
-
await page.screenshot({
|
|
122
|
-
path: join(imagesDir, `web-api-mobile-${viewport.suffix}.png`),
|
|
123
|
-
fullPage: true
|
|
124
|
-
});
|
|
125
|
-
await context.close();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
await browser.close();
|
|
130
|
-
|
|
131
|
-
console.log('\nScreenshots saved to docs/images/');
|
|
132
|
-
console.log(' - demo-main.png');
|
|
133
|
-
console.log(' - demo-messages.png');
|
|
134
|
-
console.log(' - demo-sidebar.png');
|
|
135
|
-
console.log(' - demo-channels.png');
|
|
136
|
-
console.log(' - demo-channel-messages.png');
|
|
137
|
-
console.log(' - demo-poster.png');
|
|
138
|
-
console.log(' - demo-main-mobile-390x844.png');
|
|
139
|
-
console.log(' - demo-main-mobile-360x800.png');
|
|
140
|
-
console.log(' - demo-claude-mobile-390x844.png');
|
|
141
|
-
console.log(' - demo-claude-mobile-360x800.png');
|
|
142
|
-
console.log(' - web-api-mobile-390x844.png');
|
|
143
|
-
console.log(' - web-api-mobile-360x800.png');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
captureScreenshots().catch(console.error);
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
EXPECTED_NAME="${EXPECTED_GIT_NAME:-jtalk22}"
|
|
5
|
-
EXPECTED_EMAIL="${EXPECTED_GIT_EMAIL:-james@revasser.nyc}"
|
|
6
|
-
BANNED_REGEX='(?i)(co-authored-by|generated with|\bclaude\b|\bgpt\b|\bcopilot\b|\bgemini\b|\bai\b)'
|
|
7
|
-
|
|
8
|
-
die() {
|
|
9
|
-
echo "ERROR: $*" >&2
|
|
10
|
-
exit 1
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
contains_banned_markers() {
|
|
14
|
-
local text="$1"
|
|
15
|
-
if command -v rg >/dev/null 2>&1; then
|
|
16
|
-
rg -Niq "$BANNED_REGEX" <<<"$text"
|
|
17
|
-
else
|
|
18
|
-
grep -Eiq '(Co-authored-by|Generated with|Claude|GPT|Copilot|Gemini)' <<<"$text" \
|
|
19
|
-
|| grep -Eiq '(^|[^[:alnum:]_])[Aa][Ii]([^[:alnum:]_]|$)' <<<"$text"
|
|
20
|
-
fi
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if [[ "${SKIP_LOCAL_CONFIG_CHECK:-0}" != "1" ]]; then
|
|
24
|
-
local_name="$(git config --get user.name || true)"
|
|
25
|
-
local_email="$(git config --get user.email || true)"
|
|
26
|
-
|
|
27
|
-
[[ -n "$local_name" ]] || die "Missing repo-local git user.name"
|
|
28
|
-
[[ -n "$local_email" ]] || die "Missing repo-local git user.email"
|
|
29
|
-
|
|
30
|
-
[[ "$local_name" == "$EXPECTED_NAME" ]] \
|
|
31
|
-
|| die "Repo-local user.name is '$local_name' (expected '$EXPECTED_NAME')"
|
|
32
|
-
[[ "$local_email" == "$EXPECTED_EMAIL" ]] \
|
|
33
|
-
|| die "Repo-local user.email is '$local_email' (expected '$EXPECTED_EMAIL')"
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
default_range="HEAD"
|
|
37
|
-
if git rev-parse --verify origin/main >/dev/null 2>&1; then
|
|
38
|
-
default_range="origin/main..HEAD"
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
range="${1:-${GIT_CHECK_RANGE:-$default_range}}"
|
|
42
|
-
|
|
43
|
-
git rev-list --count "$range" >/dev/null 2>&1 || die "Invalid commit range: $range"
|
|
44
|
-
commit_count="$(git rev-list --count "$range")"
|
|
45
|
-
|
|
46
|
-
if [[ "$commit_count" -eq 0 ]]; then
|
|
47
|
-
echo "No commits to validate in range '$range'."
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
errors=0
|
|
52
|
-
|
|
53
|
-
while IFS= read -r sha; do
|
|
54
|
-
author_name="$(git show -s --format=%an "$sha")"
|
|
55
|
-
author_email="$(git show -s --format=%ae "$sha")"
|
|
56
|
-
committer_name="$(git show -s --format=%cn "$sha")"
|
|
57
|
-
committer_email="$(git show -s --format=%ce "$sha")"
|
|
58
|
-
body="$(git show -s --format=%B "$sha")"
|
|
59
|
-
|
|
60
|
-
if [[ "$author_name" != "$EXPECTED_NAME" || "$author_email" != "$EXPECTED_EMAIL" ]]; then
|
|
61
|
-
echo "Commit $sha has author '$author_name <$author_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
|
|
62
|
-
errors=1
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
if [[ "$committer_name" != "$EXPECTED_NAME" || "$committer_email" != "$EXPECTED_EMAIL" ]]; then
|
|
66
|
-
echo "Commit $sha has committer '$committer_name <$committer_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
|
|
67
|
-
errors=1
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
if contains_banned_markers "$body"; then
|
|
71
|
-
echo "Commit $sha contains disallowed attribution markers in commit message." >&2
|
|
72
|
-
errors=1
|
|
73
|
-
fi
|
|
74
|
-
done < <(git rev-list "$range")
|
|
75
|
-
|
|
76
|
-
if [[ "$errors" -ne 0 ]]; then
|
|
77
|
-
exit 1
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
echo "Owner-only attribution check passed for $commit_count commit(s) in '$range'."
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
-
|
|
6
|
-
# Terms that tend to read as hype/manipulative for technical audiences.
|
|
7
|
-
DISALLOWED='(?i)(\bstealth\b|\bgrowth\b|social-proof|social proof|share kit|\bviral\b|growth loop|\bgrindset\b|\bdominate\b|hack growth|edge line|launch-ready)'
|
|
8
|
-
|
|
9
|
-
SCAN_PATHS=(
|
|
10
|
-
"$ROOT/README.md"
|
|
11
|
-
"$ROOT/docs"
|
|
12
|
-
"$ROOT/public"
|
|
13
|
-
"$ROOT/.github/ISSUE_REPLY_TEMPLATE.md"
|
|
14
|
-
"$ROOT/.github/RELEASE_NOTES_TEMPLATE.md"
|
|
15
|
-
"$ROOT/docs/COMMUNICATION-STYLE.md"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
echo "Scanning public-facing text for disallowed wording..."
|
|
19
|
-
|
|
20
|
-
if rg -Nni "$DISALLOWED" "${SCAN_PATHS[@]}"; then
|
|
21
|
-
echo "Disallowed public wording found. Use neutral reliability/compatibility language."
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
echo "Public wording check passed."
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const repoRoot = join(__dirname, "..");
|
|
9
|
-
|
|
10
|
-
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
11
|
-
const serverMeta = JSON.parse(readFileSync(join(repoRoot, "server.json"), "utf8"));
|
|
12
|
-
|
|
13
|
-
const outputArg = process.argv.includes("--out")
|
|
14
|
-
? process.argv[process.argv.indexOf("--out") + 1]
|
|
15
|
-
: "docs/release-health/version-parity.md";
|
|
16
|
-
const allowPropagation = process.argv.includes("--allow-propagation");
|
|
17
|
-
|
|
18
|
-
const mcpServerName = serverMeta.name;
|
|
19
|
-
const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
|
|
20
|
-
const smitheryListingUrl = "https://smithery.ai/server/jtalk22/slack-mcp-server";
|
|
21
|
-
|
|
22
|
-
async function fetchJson(url) {
|
|
23
|
-
const res = await fetch(url);
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
throw new Error(`HTTP ${res.status} for ${url}`);
|
|
26
|
-
}
|
|
27
|
-
return res.json();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function fetchStatus(url) {
|
|
31
|
-
const res = await fetch(url);
|
|
32
|
-
const text = await res.text().catch(() => "");
|
|
33
|
-
return { ok: res.ok, status: res.status, text };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function row(surface, version, status, note = "") {
|
|
37
|
-
return `| ${surface} | ${version || "n/a"} | ${status} | ${note} |`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function main() {
|
|
41
|
-
const localVersion = pkg.version;
|
|
42
|
-
const localServerVersion = serverMeta.version;
|
|
43
|
-
const localServerPkgVersion = serverMeta.packages?.[0]?.version || null;
|
|
44
|
-
|
|
45
|
-
let npmVersion = null;
|
|
46
|
-
let mcpRegistryVersion = null;
|
|
47
|
-
let smitheryReachable = null;
|
|
48
|
-
let smitheryStatus = null;
|
|
49
|
-
let npmError = null;
|
|
50
|
-
let mcpError = null;
|
|
51
|
-
let smitheryError = null;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`);
|
|
55
|
-
npmVersion = npmMeta?.["dist-tags"]?.latest || null;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
npmError = String(error?.message || error);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const registry = await fetchJson(
|
|
62
|
-
`https://registry.modelcontextprotocol.io/v0/servers/${encodeURIComponent(mcpServerName)}/versions/latest`
|
|
63
|
-
);
|
|
64
|
-
mcpRegistryVersion = registry?.server?.version || null;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
mcpError = String(error?.message || error);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const apiResult = await fetchStatus(smitheryEndpoint);
|
|
71
|
-
smitheryStatus = apiResult.status;
|
|
72
|
-
if (apiResult.ok) {
|
|
73
|
-
smitheryReachable = true;
|
|
74
|
-
} else if (apiResult.status === 401 || apiResult.status === 403) {
|
|
75
|
-
// Auth-gated endpoint still indicates the listing endpoint is live.
|
|
76
|
-
smitheryReachable = true;
|
|
77
|
-
} else {
|
|
78
|
-
const listingResult = await fetchStatus(smitheryListingUrl);
|
|
79
|
-
smitheryStatus = `${apiResult.status} (api), ${listingResult.status} (listing)`;
|
|
80
|
-
smitheryReachable = listingResult.ok && listingResult.text.length > 0;
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
smitheryError = String(error?.message || error);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const parityChecks = [
|
|
87
|
-
{ name: "package.json vs server.json", ok: localVersion === localServerVersion },
|
|
88
|
-
{ name: "package.json vs server.json package", ok: localVersion === localServerPkgVersion },
|
|
89
|
-
{ name: "npm latest", ok: npmVersion === localVersion },
|
|
90
|
-
{ name: "MCP registry latest", ok: mcpRegistryVersion === localVersion },
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
const externalMismatches = parityChecks
|
|
94
|
-
.filter((check) => !check.ok && (check.name === "npm latest" || check.name === "MCP registry latest"));
|
|
95
|
-
const hardFailures = parityChecks
|
|
96
|
-
.filter((check) => !check.ok && check.name !== "npm latest" && check.name !== "MCP registry latest");
|
|
97
|
-
|
|
98
|
-
const now = new Date().toISOString();
|
|
99
|
-
const lines = [
|
|
100
|
-
"# Version Parity Report",
|
|
101
|
-
"",
|
|
102
|
-
`- Generated: ${now}`,
|
|
103
|
-
`- Local target version: ${localVersion}`,
|
|
104
|
-
"",
|
|
105
|
-
"## Surface Matrix",
|
|
106
|
-
"",
|
|
107
|
-
"| Surface | Version | Status | Notes |",
|
|
108
|
-
"|---|---|---|---|",
|
|
109
|
-
row(
|
|
110
|
-
"package.json",
|
|
111
|
-
localVersion,
|
|
112
|
-
"ok"
|
|
113
|
-
),
|
|
114
|
-
row(
|
|
115
|
-
"server.json (root)",
|
|
116
|
-
localServerVersion,
|
|
117
|
-
localServerVersion === localVersion ? "ok" : "mismatch"
|
|
118
|
-
),
|
|
119
|
-
row(
|
|
120
|
-
"server.json (package entry)",
|
|
121
|
-
localServerPkgVersion,
|
|
122
|
-
localServerPkgVersion === localVersion ? "ok" : "mismatch"
|
|
123
|
-
),
|
|
124
|
-
row(
|
|
125
|
-
"npm dist-tag latest",
|
|
126
|
-
npmVersion,
|
|
127
|
-
npmVersion === localVersion ? "ok" : "mismatch",
|
|
128
|
-
npmError ? `fetch_error: ${npmError}` : ""
|
|
129
|
-
),
|
|
130
|
-
row(
|
|
131
|
-
"MCP Registry latest",
|
|
132
|
-
mcpRegistryVersion,
|
|
133
|
-
mcpRegistryVersion === localVersion ? "ok" : "mismatch",
|
|
134
|
-
mcpError ? `fetch_error: ${mcpError}` : ""
|
|
135
|
-
),
|
|
136
|
-
row(
|
|
137
|
-
"Smithery endpoint",
|
|
138
|
-
"n/a",
|
|
139
|
-
smitheryReachable ? "reachable" : "unreachable",
|
|
140
|
-
smitheryError
|
|
141
|
-
? `check_error: ${smitheryError}`
|
|
142
|
-
: `status: ${smitheryStatus ?? "unknown"}; version check is manual.`
|
|
143
|
-
),
|
|
144
|
-
"",
|
|
145
|
-
"## Interpretation",
|
|
146
|
-
"",
|
|
147
|
-
hardFailures.length === 0
|
|
148
|
-
? "- Local metadata parity: pass."
|
|
149
|
-
: `- Local metadata parity: fail (${hardFailures.map((f) => f.name).join(", ")}).`,
|
|
150
|
-
externalMismatches.length === 0
|
|
151
|
-
? "- External parity: pass."
|
|
152
|
-
: `- External parity mismatch: ${externalMismatches.map((f) => f.name).join(", ")}.`,
|
|
153
|
-
externalMismatches.length === 0
|
|
154
|
-
? "- Propagation mode: not needed (external parity is already aligned)."
|
|
155
|
-
: (allowPropagation
|
|
156
|
-
? "- Propagation mode enabled: external mismatch accepted temporarily."
|
|
157
|
-
: "- Propagation mode disabled: external mismatch is a release gate failure."),
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
const outPath = join(repoRoot, outputArg);
|
|
161
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
162
|
-
writeFileSync(outPath, `${lines.join("\n")}\n`, "utf8");
|
|
163
|
-
console.log(`Wrote ${outputArg}`);
|
|
164
|
-
|
|
165
|
-
if (hardFailures.length > 0) {
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
if (!allowPropagation && externalMismatches.length > 0) {
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
main().catch((error) => {
|
|
174
|
-
console.error(error);
|
|
175
|
-
process.exit(1);
|
|
176
|
-
});
|